Changes to 'refs/tags/v2.0'
by Pierre-YvesChibon
Tag 'v2.0' created by Pierre-Yves Chibon <pingou(a)pingoured.fr> at 2014-07-16 07:09 +0000
Release 2.0
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1
iEYEABECAAYFAlPGJSMACgkQ2HRbBxDocwoiiACcDLLcVb6o6snh0IjFvRKa/lb5
DAMAn3MDqby6Xhvp8xj+FXnrDj3E2T9d
=XZPo
-----END PGP SIGNATURE-----
Changes since the dawn of time:
Frank Chiulli (21):
httpd/wsgi now working
Change for RPM
Change for RPM
Changes to support building a good package
Fix no elections issue
Add elections owner to db
Specify dummy FAS account
Fix db issues
Add task
Fix processing 'My Elections'
Add constraint to votes table
Update instructions
Restructure.
Create simple vote election
More simple voting updates
Add info to clone git repo
Update task list
Update version
Update directory path
Update directory path
Updates for new version
Ian Weller (2):
Initial commit with mostly-working admin
List frontpaged elections on front page
Kamal Ejaz (1):
Add lines in Admin and vote lib
Luca Foppiano (2):
Close ticket #1 : add default sort order to candidates and to votes
Fixed #6: Write documentation
Mark Chappell (13):
Redirect to the edit page when you create an election
https://fedorahosted.org/elections/ticket/26
Add candidates through the Election Edit page
Split the main elections lists into 3, Past, Present, Future
initial code for election 'admins', so far this only allows the bypassing of an embargo
Add in code to have per election 'admins'
When editing election reference them by id.
Replace the heading in newc and newe to be more descriptive than 'Welcome to Turbogears'
Bump version up to 0.4 prior to rebuild
use tg.checker rather than a nasty if clause
put something useful into the changelog
Add new admins to ElectionAdmins rather than LegalVoters. Replace \t with ' '
Add a little more detail to the changelog
Nigel Jones (89):
mv dev.cfg & ignore
Small cleanup
Schema/Model:
Controllers:
Controllers:
Close ticket #2 (Vote confirmation page)
Update svn:ignore & start controller for editing
WIP - Edit Elections
Implement Range Voting!
Comment out the debugger, enable date checks
SCHEMA CHANGE
Gaa, handle ValueError for non integers
More changes... a results page\!
Silly mistake
Add some sanity checking
Add election and election_started to info's dict so we can disable the form because the election starts, also show results if the election has ended.
Just some WIP stuff relating to creating elections + string correction
Some working signs of adding elections... needs A LOT of work
FAS Identity Configured
Slight corrections but otherwise... IT WORKS
More identity checking - WIP
New Files - WSGI Voodoo and COPYING (GPL 2)
Additions/Deletions:
Opps, missed this one
Renames:
baseurl->tg.url changes and a small tweak to setup.py
Misc Bug Fixes
Okay, I was doing it all wrong...
Template changes, why have info links if they goto self, I think this is the cause of the 500 errors
* elections.sql
Small template name changes
Oppsie
* controllers.py:
Something went wack, I was getting DOS EOLs
Update to 0.1.0!
Small spelling corrections
More bugfixes
Merge Rickys fix and fix links as reported to webmaster
Controllers update to close Ticket #18 \(Typo after voting has been done\)
String and CSS updates
Hmmm the typo dtimms found didn't find it's way back to master.
Schema changes
Setup .gitignore
.gitignore & schema changes
Make adding candidates easier
Embargoed entry...
Add check for 'anycla' (any group bar CLA)
gitignore update
Add embargoed check
Old spec file...
Bump to 0.1.5
Update manifest.in
Fix a little oopsie!
Schema oopsie
WIP
Just to be sure...
SP Fix...
Merge branch '0.1.5'
Change where fas2 is loaded from (API change)
Make some small adjustments
Update SQL schema
I hate .txt....
Clean UPDATE-NOTES
Split off /vote/ this won't work yet...
Actually, this might just work
Readapt /admin/newe/ (new election form) and / (main election list) for new schema, also fix up SQL schema
Fix up adding new candidates
Make /about/foo work again...
Votes can now be recorded
And now one can see the results... everything now uses the new schema (I think)
Fix SQL for timestamps...
Better way to determine if to translate candidate names via FAS or not
Import FAS correctly
Grab Candidate's name from FAS when election is set to usefas - part 1
Grab Candidate's name from FAS when election is set to usefas - part 2/2
Get rid of /admin/ and make admin stuff API only...
Fix admnewe.html for now, fix API (thanks Toshio)
Update to about.html to start nomination stuff, also dos2unix a heap of files...
Commit minor edits to SQL
Just a tiny change
Random fix ups to make the current code work as the old did.
Changes/updates to SQL schema
Minor update again to bring it in line with whats actually required.
Fix missing comma
Merge branch 'master' of ssh://fedorahosted.org/git/elections
Bump to 0.2.0 for production
Missing import (oops!)
Add a verification page to the elections application.
[Ticket #30] Selected the wrong field - bad mistake.
Pierre-Yves Chibon (128):
Import port of Elections to TG2.
Let's rely on python-fedora-turbogears2 rather than python-fedora-turbogears
Enhance documentation on how to setup elections
Let's do the default DB in sqlite
Remove old prints and comment out admin not used
Remove prints and rework the entry into the database of the dates
Start porting the model to SA
Start removing View from the application.
Code layout in the template
Remove everything so that we can put the flask port on the top of this
Fix the forms to use directly wtforms instead of flask-wtf
Do not run on port 80 by default as this will conflict with any apache server running
Remove the sample createdb.in and the corresponding python script
Populate the default config with the values present in the sample development configuration file
Remove un-necessary configuraiton file now that everything is moved to the default_config
Remove queries from the form
We cannot have 0 elections, this is taken care of in the controller
We cannot have 0 elections, this is taken care of in the controller
We cannot have 0 elections, this is taken care of in the controller
Rework the models.py file to port it to plain SQLAlchemy
Port the flask application away from flask-sqlalchemy
We no longer have a development configuration file to use on the tests
Adjust the tests to run on the current application
Add script to easily create the database required to work on elections
Add decorator when login is required
Adjust requirements
Bug fix in Vote.of_user_on_election() method to account for the count argument
Fix retrieving a valid election that have ended
Fix typo when retrieving a valid election
Adjust the runserver script
Reduce indentation
pep8 fixes
Style change
Port from flask-fas to flask-fas-openid
Adjust the requirements to list python-fedora and not flask-fas
Remove the login form, no longer needed
Little clean up in the master template
Remove the login template, no longer used
Add some missing dependencies
Fix link, we have auth_login not login here
Create a dedicated function to check is the user is an admin
Add a top level __version__ and use this in the setup.py
Inject some variables into every templates
If there is no elections, keep going this will be our admin index page
Add the election logo
Add the images used by jQuery
Add jQuery and our jQuery UI
Add jQuery CSS
Add the koji.css our base and the elections.css dedicated to elections
Update the base template to the new theme
Inform the user if we have no election on the front page
Update the template used as front page of the admin section
Add dedicated macro to present the fields of a form in row
Fix the page for adding a new candidate
Rework the add election form
CSS fixes: bring back the coloring when validating a vote - remove unused css
Remove images we will no longer use
Rework the add election form
Update the template used as front page of the admin section
Fix the Election.get_open_election method and add Election.get_next_election
Add tags to display the menu correctly
Change the front page to display some information about the current, future and past elections
Small css fix to give a little space around the tables
Remove the frontpage boolean/checkbox from the form as we no longer use it
Add a little text to the front page of elections
Update the template used as front page of the admin section
Add form to add multiple candidates in one go
Add the template to add multiple candidates to an election
Add the controller to import multiple candidates in one go
Add link on the admin page of an election to import multiple candidates
Specify clearly that URL are optional when uploading multiple candidates
When running elections2 locally, activate the debug mode
Mark with a red starts the field mandatory when creating an election
Small style change and add information message when displaying embargoed results
The FAS client is FAS2 not fas2
Add method in the Vote class to retrieve some stats on a specified elections
Provide in the election result page some statistics about the election
Fix the status property of an election
Fix the FAS2 client insecure settings
Clean configuration file
Rename the is_elections_admin function to is_admin
Add a generic is_election_admin checking if the user is in the admin group set for an election
No need for the runserver script, elections can now run with the default configuration
Move the spec file and the sample configuration file into the files folder
Ignore .egg-info files in the git
Remove the no-longer needed configure script
The apache configuration file should be commented out by default
Rename the example apache configuration file
Small adjustments to the example wsgi file
Rename the example wsgi file for fedora_elections
Remove the `shell` script now that there is no longer development.cfg file
Remove configuration key that is not used anywhere
Update the setup.py script
Include a MANIFEST.in files listing which files should be present in the releases
Update the spec file to the changes made in the setup.py and directory structure
Ignore the build and dist folders from the sources
Simplify the .gitignore file
Add example configuration file explaining as well all the configuration keys
The spec file has moved to the files folder
Fix the createdb.py script regarding jinja2
Adjust the spec file to correspond to the configuration file
Include /usr/share/fedora-elections/ in the spec file
The first release will need to be 2.0
Rename the field `election.summary` into `election.shortdesc`
Remove the frontpage field in the elections table
Rename the number_elected back to seats_elected in the elections table
Change the embargoed field from Boolean to Integer
Remove the `role_required` field in the electionadmingroups table
Change the __tablename__ from electionadmingroups to electionadmins
Fix the order_by date to order in the dates in descending order
Fix the default title in the base master
Give the total number of candidates in the view_election table
Cover case where there has been 0 votes, thus fixing the division by 0
When importing multiple candidates flash the number of candidates imported
pep8 fixes
Fix import the configuration file specified by the environment variable
Fix ordering of the imports
Add sql upgrade script, allowing to convert the database from elections v1 to v2
Add a dedicated is_authenticated method and use it in the decorators
Set flask.g.fas_user to None upon logout
If the user is authenticated, only show election that the user can vote on
Order the results returned by elections.search() by election date (most recent first)
List all the elections to the admins
Adjust the `Vote now!` links in the open elections tab
Little css fix to have the same padding on each side of a table cell
When retrieving vote counts, only consider votes above 0
Install the update_1_to_2.sql upgrade sql script to /usr/share/fedora-elections
Update the changelog date for 2.0 and changelog content
Pierre-Yves Chibon py(a)chibon.fr (2):
Clean root controller and add log in mechanism for TG2
Clean and add authentication constraints
Ralph Bean (7):
Added preliminary requirements.txt.
Move kitchen above python-fedora in the requirements.txt list.
Add a fedmsg development config.
Add fedmsg to the list of requirements.
Emit a fedmsg message when a new election is created.
Revert "Add fedmsg to the list of requirements."
Make fedmsg optional with a shim module.
Ricky Zhou (7):
Initial import.
Initial theming.
tg => turbogears
Fix other bug.
One more tg => turbogears
Add missing comma.
Add MANIFEST.in
Toshio Kuratomi (26):
Preliminary port to using jsonfas2 with csrf protection.
Switch apache config to Location
Don't need SABase here.
Fix import of python-fedora login/logout controller methods.
Fix parameter and variable to be the same
Return the appTitle as well
Move config from the wsgi script into the config file.
Fix erroneous removal of tg_flash from the master template.
Don't error out if human_name is unavailable
Include spec and ini file in the tarball
Remove all rights reserved from source and footer of page
Correct typo in string
Bring back the elections.sql file
Make the elections administration group configurable
Merge branch 'master' of git+ssh://git.fedorahosted.org/git/elections
Add a TODO file
Add unittests -- currently just a few smoketests
Fix traceback for hitting logout when the user is not logged in
Add ability to specify site-specifc css
Smoketesting now covers all urls' GET method.
A few more todo items: i18n, more paranoid error handling
Add two new pages for dsmoketesting
Add an implementation of a page about an election
Some more pages and functions that are blockers
Merge the about page into the vote page
Rest of the changes to merge about into vote
fchiulli (6):
Lots of updates/changes
Test git push
Test again
Fix AccountSystem call
Update documentation
Correct version
tremble (7):
Early bashes at an admin homepage
Also catch AuthErrors, generally caused by not being able to fully log into FAS
Controller for early bash at an admin homepage
Add the magic to look up FAS Group names
Final first hash of editing an election
Cope slightly better with a lack of alias when displaying lists of elections
More coping with missing aliases
9 years, 10 months
Branch 'develop' - 376 commits - alembic/env.py alembic/script.py.mako alembic/versions AUTHORS COPYING createdb.py fedmsg.d/elections.py fedora_elections/admin.py fedora_elections/default_config.py fedora_elections/elections.py fedora_elections/fedmsgshim.py fedora_elections/forms.py fedora_elections/__init__.py fedora_elections/models.py fedora_elections/proxy.py fedora_elections/static fedora_elections/templates files/alembic.ini files/fedora-elections.cfg files/fedora-elections.conf fi
by Pierre-YvesChibon
Rebased ref, commits from common ancestor:
commit b2e5f17a90146dc0d70b12633c2b4103c465f258
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat Jul 12 00:07:21 2014 +0200
Make the README file an rst file
diff --git a/README b/README
deleted file mode 100644
index 42dc6de..0000000
--- a/README
+++ /dev/null
@@ -1,132 +0,0 @@
-================
-Fedora Elections
-================
-
-* What is fedora-election?
-
-fedora-elections is a web application written in python and based on flask.
-It implements the Range Voting system (http://rangevoting.org).
-
-This project was developed using fedoraproject requests, but, can be easily
-adapted to other projects. Fedora-elections is integrated with the Fedora
-Account System (FAS).
-
-
-Running from a checkout:
-========================
-
-Install Prerequisites
----------------------
-Before launching fedora-elections, the following packages should be installed:
- - httpd
- - libxslt
- - python
- - python-backports-ssl_match_hostname
- - python-bunch
- - python-chardet
- - python-fedora
- - python-fedora-flask
- - python-flask
- - python-flask-sqlalchemy
- - python-flask-wtf
- - python-jinja2
- - python-kitchen
- - python-lxml
- - python-openid
- - python-ordereddict
- - python-ordereddict
- - python-setuptools
- - python-simplejson
- - python-six
- - python-sqlalchemy0.7
- - python-urllib3
- - python-wtforms
-
-
-Get the source code
--------------------
-The project is hosted on https://fedorahosted.org/
-
-More precisely at: https://fedorahosted.org/elections
-
-You can obtain the code via:
-
- ::
-
- git clone http://git.fedorahosted.org/git/elections.git
-
-For commodity reason, a clone is available on github:
-https://github.com/fedora-infra/elections
-
-
-Configure the application
--------------------------
-An example configuration file is provided at: ``files/fedora-elections.cfg``
-
-
-Create a database
------------------
-Run:
-
- ::
-
- python createdb.py
-
-
-Starting the Application
-------------------------
-
-There are 2 ways to start the application:
- * without apache
- * with apache
-
-
-* How to start without apache on localhost:5000 (useful for development):
-
- ::
-
- ./runserver
-
-
-* How to start with http
-
- Next copy the file ``fedora-elections.conf`` file to your apache conf.d
- directory:
-
- ::
-
- sudo cp files/fedora-elections.conf /etc/httpd/conf.d/.
-
- Place the file ``fedora-elections.wsgi`` for example in /var/www
-
- ::
-
- sudo cp files/fedora-elections.wsgi /var/www
-
- Adjust the apache configuration file to point to it
-
- Adjust the wsgi file installed in /var/www to point to fedora_elections
-
-
- Place the fedora-elections configuration file in
- ``/etc/fedora-elections/fedora-elections.cfg``
-
- ::
-
- sudo mkdir -p /etc/fedora-elections/
- sudo cp files/fedora-elections.cfg /etc/fedora-elections/
-
- Restart apache:
-
- sudo /etc/init.d/httpd restart
-
-* How to contribute
-
-If you find bug or want to propose ideas or stuff to be implemented or if
-you are interested to became a developer for this project just
-ask on #fedora-admin irc channel on irc.freenode.net or use our
-web site https://fedorahosted.org/elections.
-
-* Licence
-
-fedora-elections is licenced under GPL v2.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..42dc6de
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,132 @@
+================
+Fedora Elections
+================
+
+* What is fedora-election?
+
+fedora-elections is a web application written in python and based on flask.
+It implements the Range Voting system (http://rangevoting.org).
+
+This project was developed using fedoraproject requests, but, can be easily
+adapted to other projects. Fedora-elections is integrated with the Fedora
+Account System (FAS).
+
+
+Running from a checkout:
+========================
+
+Install Prerequisites
+---------------------
+Before launching fedora-elections, the following packages should be installed:
+ - httpd
+ - libxslt
+ - python
+ - python-backports-ssl_match_hostname
+ - python-bunch
+ - python-chardet
+ - python-fedora
+ - python-fedora-flask
+ - python-flask
+ - python-flask-sqlalchemy
+ - python-flask-wtf
+ - python-jinja2
+ - python-kitchen
+ - python-lxml
+ - python-openid
+ - python-ordereddict
+ - python-ordereddict
+ - python-setuptools
+ - python-simplejson
+ - python-six
+ - python-sqlalchemy0.7
+ - python-urllib3
+ - python-wtforms
+
+
+Get the source code
+-------------------
+The project is hosted on https://fedorahosted.org/
+
+More precisely at: https://fedorahosted.org/elections
+
+You can obtain the code via:
+
+ ::
+
+ git clone http://git.fedorahosted.org/git/elections.git
+
+For commodity reason, a clone is available on github:
+https://github.com/fedora-infra/elections
+
+
+Configure the application
+-------------------------
+An example configuration file is provided at: ``files/fedora-elections.cfg``
+
+
+Create a database
+-----------------
+Run:
+
+ ::
+
+ python createdb.py
+
+
+Starting the Application
+------------------------
+
+There are 2 ways to start the application:
+ * without apache
+ * with apache
+
+
+* How to start without apache on localhost:5000 (useful for development):
+
+ ::
+
+ ./runserver
+
+
+* How to start with http
+
+ Next copy the file ``fedora-elections.conf`` file to your apache conf.d
+ directory:
+
+ ::
+
+ sudo cp files/fedora-elections.conf /etc/httpd/conf.d/.
+
+ Place the file ``fedora-elections.wsgi`` for example in /var/www
+
+ ::
+
+ sudo cp files/fedora-elections.wsgi /var/www
+
+ Adjust the apache configuration file to point to it
+
+ Adjust the wsgi file installed in /var/www to point to fedora_elections
+
+
+ Place the fedora-elections configuration file in
+ ``/etc/fedora-elections/fedora-elections.cfg``
+
+ ::
+
+ sudo mkdir -p /etc/fedora-elections/
+ sudo cp files/fedora-elections.cfg /etc/fedora-elections/
+
+ Restart apache:
+
+ sudo /etc/init.d/httpd restart
+
+* How to contribute
+
+If you find bug or want to propose ideas or stuff to be implemented or if
+you are interested to became a developer for this project just
+ask on #fedora-admin irc channel on irc.freenode.net or use our
+web site https://fedorahosted.org/elections.
+
+* Licence
+
+fedora-elections is licenced under GPL v2.
commit 177eb298c8c4e0251d48dbb58ee1dce1487e0f8d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat Jul 12 00:06:02 2014 +0200
Update the documentation in the README file
diff --git a/README b/README
index 4ab94e0..42dc6de 100644
--- a/README
+++ b/README
@@ -1,8 +1,9 @@
================
Fedora Elections
-===============
+================
* What is fedora-election?
+
fedora-elections is a web application written in python and based on flask.
It implements the Range Voting system (http://rangevoting.org).
@@ -12,7 +13,7 @@ Account System (FAS).
Running from a checkout:
-=======================
+========================
Install Prerequisites
---------------------
@@ -44,57 +45,88 @@ Before launching fedora-elections, the following packages should be installed:
Get the source code
-------------------
-You can obtain the code from github:
- https://github.com/fedora-infra/elections.git
+The project is hosted on https://fedorahosted.org/
+
+More precisely at: https://fedorahosted.org/elections
+
+You can obtain the code via:
+
+ ::
+
+ git clone http://git.fedorahosted.org/git/elections.git
-git clone https://github.com/fedora-infra/elections.git
+For commodity reason, a clone is available on github:
+https://github.com/fedora-infra/elections
Configure the application
-------------------------
-cd fedora_elections
-../configure.py --install-conf=${PWD%/*} --install-data=${PWD%/*} --shared-state=${PWD%/*}
+An example configuration file is provided at: ``files/fedora-elections.cfg``
-This will create createdb, fedora-elections.cfg, fedora-elections.wsgi and
-http-fedora-elections.conf.
-
-Modify fedora-elections.cfg to change FAS_PASSWORD to the correct password.
Create a database
-----------------
-Copy development.cfg.sample to development.cfg.
-Review the file and make changes as appropriate.
Run:
- ./createdb
+
+ ::
+
+ python createdb.py
Starting the Application
------------------------
There are 2 ways to start the application:
- o without http
- o with http
+ * without apache
+ * with apache
-* How to start without http on localhost:5000:
- cd elections
+* How to start without apache on localhost:5000 (useful for development):
+
+ ::
+
./runserver
* How to start with http
- Next copy the httpd-pkgdb.conf file to your apache conf.d directory:
- sudo cp httpd-fedora-elections.conf /etc/httpd/conf.d/.
+ Next copy the file ``fedora-elections.conf`` file to your apache conf.d
+ directory:
+
+ ::
+
+ sudo cp files/fedora-elections.conf /etc/httpd/conf.d/.
+
+ Place the file ``fedora-elections.wsgi`` for example in /var/www
+
+ ::
+
+ sudo cp files/fedora-elections.wsgi /var/www
+
+ Adjust the apache configuration file to point to it
+
+ Adjust the wsgi file installed in /var/www to point to fedora_elections
+
+
+ Place the fedora-elections configuration file in
+ ``/etc/fedora-elections/fedora-elections.cfg``
+
+ ::
+
+ sudo mkdir -p /etc/fedora-elections/
+ sudo cp files/fedora-elections.cfg /etc/fedora-elections/
Restart apache:
sudo /etc/init.d/httpd restart
* How to contribute
+
If you find bug or want to propose ideas or stuff to be implemented or if
you are interested to became a developer for this project just
ask on #fedora-admin irc channel on irc.freenode.net or use our
web site https://fedorahosted.org/elections.
* Licence
+
fedora-elections is licenced under GPL v2.
commit d5f78c9ac9d4fb6071198106308c44db3cefe3bb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 17:24:45 2014 +0200
Ensure adding multiple users are once are FAS users when adding them (if election is set to)
Fixes to https://fedorahosted.org/elections/ticket/34
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index 5ddb80f..ad7f072 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -290,6 +290,20 @@ def admin_add_multi_candidate(election_alias):
candidates_name = []
for entry in form.candidate.data.strip().split("|"):
candidate = entry.split("!")
+
+ if election.candidates_are_fasusers: # pragma: no cover
+ try:
+ FAS2.person_by_username(candidate[0])['human_name']
+ except (KeyError, AuthError), err:
+ SESSION.rollback()
+ flask.flash(
+ 'User `%s` does not have a FAS account.'
+ % candidate[0], 'error')
+ return flask.redirect(
+ flask.url_for(
+ 'admin_add_candidate',
+ election_alias=election_alias))
+
# No url
if len(candidate) == 1:
cand = models.Candidate(
commit 019b3a74af77be421fcc773e3a0b0b3fbcf82bb4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 17:20:23 2014 +0200
Check that new user are FAS users when adding them (if election is set to)
Relates to https://fedorahosted.org/elections/ticket/34
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index dd1abb0..5ddb80f 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -29,12 +29,13 @@ from functools import wraps
import flask
from sqlalchemy.exc import SQLAlchemyError
+from fedora.client import AuthError
from fedora_elections import fedmsgshim
from fedora_elections import forms
from fedora_elections import models
from fedora_elections import (
- APP, SESSION, is_authenticated, is_admin, is_election_admin,
+ APP, SESSION, FAS2, is_authenticated, is_admin, is_election_admin,
is_safe_url, safe_redirect_back
)
@@ -236,6 +237,19 @@ def admin_add_candidate(election_alias):
form = forms.CandidateForm()
if form.validate_on_submit():
+
+ if election.candidates_are_fasusers: # pragma: no cover
+ try:
+ FAS2.person_by_username(form.name.data)['human_name']
+ except (KeyError, AuthError), err:
+ flask.flash(
+ 'User `%s` does not have a FAS account.'
+ % form.name.data, 'error')
+ return flask.redirect(
+ flask.url_for(
+ 'admin_add_candidate',
+ election_alias=election_alias))
+
candidate = models.Candidate(
election=election,
name=form.name.data,
commit 443a5354f6328638d68ed785d9375c54d8d87632
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:35:14 2014 +0200
Do not include FAS integration in test coverage
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 345056e..46c14ee 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -140,7 +140,8 @@ def get_simple_voting_form(candidates, fasusers):
titles = []
for candidate in candidates:
title = candidate.name
- if fasusers:
+ if fasusers: # pragma: no cover
+ # We can't cover FAS integration
try:
title = \
FAS2.person_by_username(candidate.name)['human_name']
commit 9231479a568eabab067c2e4409542e35554c60b6
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:32:25 2014 +0200
Add unit-tests for the is_safe_url method
diff --git a/tests/test_flask.py b/tests/test_flask.py
index 0f39e3e..b9da2cd 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -43,6 +43,22 @@ from tests import ModelFlasktests, FakeUser, user_set
class Flasktests(ModelFlasktests):
""" Flask application tests. """
+ def test_is_safe_url(self):
+ """ Test the is_safe_url function. """
+ app = flask.Flask('elections')
+
+ with app.test_request_context():
+ self.assertTrue(
+ fedora_elections.is_safe_url('http://localhost'))
+ self.assertTrue(
+ fedora_elections.is_safe_url('https://localhost'))
+ self.assertTrue(
+ fedora_elections.is_safe_url('http://localhost/test'))
+ self.assertFalse(
+ fedora_elections.is_safe_url('http://fedoraproject.org/'))
+ self.assertFalse(
+ fedora_elections.is_safe_url('https://fedoraproject.org/'))
+
def test_index_empty(self):
""" Test the index function. """
output = self.app.get('/')
commit 987452eb2e799ab3c9555fe12f45bb4d86a6121e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:18:11 2014 +0200
Add unit-tests for select voting
diff --git a/tests/test_flask_select_voting.py b/tests/test_flask_select_voting.py
new file mode 100644
index 0000000..cc248b3
--- /dev/null
+++ b/tests/test_flask_select_voting.py
@@ -0,0 +1,174 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections.elections test script
+"""
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import logging
+import unittest
+import sys
+import os
+
+from datetime import time
+from datetime import timedelta
+
+import flask
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+import fedora_elections
+from tests import ModelFlasktests, Modeltests, TODAY, FakeUser, user_set
+
+
+# pylint: disable=R0904
+class FlaskSimpleElectionstests(ModelFlasktests):
+ """ Flask application tests range voting. """
+
+ def test_vote_select(self):
+ """ Test the vote_select function - the preview part. """
+ output = self.app.get(
+ '/vote/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser(['packager'], username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/vote/test_election6', follow_redirects=True)
+ self.assertTrue(
+ 'class="message">You have already voted in the election!</'
+ in output.data)
+
+ user = FakeUser(['packager'], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/vote/test_election6')
+ self.assertTrue(
+ '<h2>test election 6 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="hidden" name="action" value="preview" />'
+ in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ # Invalid vote: No candidate
+ data = {
+ 'action': 'preview',
+ }
+
+ output = self.app.post('/vote/test_election6', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 6 shortdesc</h2>' in output.data)
+
+ # Invalid vote: Too many candidates
+ data = {
+ 'Kevin': True,
+ 'Toshio': True,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/vote/test_election6', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 6 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="hidden" name="action" value="preview" />'
+ in output.data)
+ self.assertTrue(
+ '<li class="error">Too many candidates submitted</li>'
+ in output.data)
+
+ # Valid input
+ data = {
+ 'Kevin': True,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/vote/test_election6', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 6 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="hidden" name="action" value="submit" />'
+ in output.data)
+ self.assertTrue(
+ '<li class="message">Please confirm your vote!</li>'
+ in output.data)
+
+ def test_vote_select_process(self):
+ """ Test the vote_select function - the voting part. """
+ output = self.app.get(
+ '/vote/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser(['packager'], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ # Invalid candidate id - no csrf
+ data = {
+ 'candidate': 1,
+ 'action': 'submit',
+ }
+
+ output = self.app.post(
+ '/vote/test_election6', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ # Valid input
+ data = {
+ 'Toshio': True,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/vote/test_election6', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ 'class="message">Your vote has been recorded. Thank you!</'
+ in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(FlaskSimpleElectionstests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit 275f76159479f6827e4eff3ba572df706422a93d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:17:51 2014 +0200
Add a max_votes to 2 for the range voting and adjust the unit-tests accordingly
diff --git a/tests/test_election.py b/tests/test_election.py
index 024af37..2370716 100644
--- a/tests/test_election.py
+++ b/tests/test_election.py
@@ -102,6 +102,7 @@ class Electiontests(Modeltests):
embargoed=1,
voting_type='range',
candidates_are_fasusers=0,
+ max_votes=2,
fas_user='pingou',
)
self.session.add(obj)
diff --git a/tests/test_flask_range_voting.py b/tests/test_flask_range_voting.py
index bf6cb95..0cc94fd 100644
--- a/tests/test_flask_range_voting.py
+++ b/tests/test_flask_range_voting.py
@@ -108,7 +108,7 @@ class FlaskRangeElectionstests(ModelFlasktests):
# Invalid vote: too low
data = {
'9': -1,
- '6': 3,
+ '6': 0,
'5': 2,
'action': 'preview',
'csrf_token': csrf_token,
@@ -125,7 +125,7 @@ class FlaskRangeElectionstests(ModelFlasktests):
output.data.count('<td class="error">Not a valid choice</td>'),
1)
- # Invalid vote: too high
+ # Invalid vote: 2 are too high
data = {
'9': 5,
'6': 3,
@@ -143,12 +143,12 @@ class FlaskRangeElectionstests(ModelFlasktests):
in output.data)
self.assertEqual(
output.data.count('<td class="error">Not a valid choice</td>'),
- 1)
+ 2)
# Invalid vote: Not numeric
data = {
'9': 'a',
- '6': 3,
+ '6': 0,
'5': 2,
'action': 'preview',
'csrf_token': csrf_token,
@@ -168,7 +168,7 @@ class FlaskRangeElectionstests(ModelFlasktests):
# Valid input
data = {
'4': 1,
- '6': 3,
+ '6': 0,
'5': 2,
'action': 'preview',
'csrf_token': csrf_token,
@@ -182,7 +182,7 @@ class FlaskRangeElectionstests(ModelFlasktests):
'<input type="hidden" name="action" value="submit" />'
in output.data)
self.assertTrue('<div class="vtmedcool">' in output.data)
- self.assertTrue('<div class="vthot">' in output.data)
+ self.assertTrue('<div class="vtcold">' in output.data)
self.assertTrue('<div class="vtmedwarm">' in output.data)
def test_vote_range_process(self):
@@ -215,10 +215,24 @@ class FlaskRangeElectionstests(ModelFlasktests):
csrf_token = output.data.split(
'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+ # Invalid vote: invalid username
+ data = {
+ 'Toshio': 1,
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'submit',
+ }
+
+ output = self.app.post('/vote/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 3)
+
# Invalid vote: too low
data = {
'4': -1,
- '5': 3,
+ '5': 0,
'6': 2,
'action': 'submit',
'csrf_token': csrf_token,
@@ -231,7 +245,7 @@ class FlaskRangeElectionstests(ModelFlasktests):
output.data.count('<td class="error">Not a valid choice</td>'),
1)
- # Invalid vote: too high
+ # Invalid vote: 2 are too high
data = {
'4': 5,
'5': 3,
@@ -245,12 +259,12 @@ class FlaskRangeElectionstests(ModelFlasktests):
self.assertEqual(output.status_code, 200)
self.assertEqual(
output.data.count('<td class="error">Not a valid choice</td>'),
- 1)
+ 2)
# Invalid vote: Not numeric
data = {
'4': 'a',
- '5': 3,
+ '5': 0,
'6': 2,
'action': 'submit',
'csrf_token': csrf_token,
@@ -266,7 +280,7 @@ class FlaskRangeElectionstests(ModelFlasktests):
# Valid input
data = {
'4': 1,
- '5': 3,
+ '5': 0,
'6': 2,
'action': 'submit',
'csrf_token': csrf_token,
commit e19da887631ab8a24be57cdee7eb8dbcc8482a53
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:17:05 2014 +0200
Remove checking if the candidate submitted is in the list as wtforms handles it for us
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 9a3fcd7..2b5c407 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -147,10 +147,6 @@ def vote_range(election):
if candidate.short_name in ['csrf_token', 'action']:
continue
- if candidate.short_name not in cand_ids:
- flask.flash('Invalid input submitted', 'error')
- return safe_redirect_back()
-
new_vote = models.Vote(
election_id=election.id,
voter=flask.g.fas_user.username,
commit 271506f5a8f7947d944fbed197b333ef8192eb37
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:05:09 2014 +0200
Set a max_votes to election #6
diff --git a/tests/test_election.py b/tests/test_election.py
index 90037ed..024af37 100644
--- a/tests/test_election.py
+++ b/tests/test_election.py
@@ -165,6 +165,7 @@ class Electiontests(Modeltests):
embargoed=1,
voting_type='select',
candidates_are_fasusers=0,
+ max_votes=1,
fas_user='skvidal',
)
self.session.add(obj)
commit e13214a162132d7906fe9907633903ea29596419
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:04:45 2014 +0200
Adjust the unit-tests now that Toshio voted on the election #6
diff --git a/tests/test_flask.py b/tests/test_flask.py
index 38c9961..0f39e3e 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -83,7 +83,7 @@ class Flasktests(ModelFlasktests):
self.assertTrue('<a href="/vote/' in output.data)
self.assertTrue(
'<span class="text">logged in as </span>' in output.data)
- self.assertTrue('Vote now!' in output.data)
+ self.assertEqual(output.data.count('Vote now!'), 3)
user = FakeUser([], username='toshio')
with user_set(fedora_elections.APP, user):
@@ -95,7 +95,7 @@ class Flasktests(ModelFlasktests):
self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
self.assertTrue(
'<span class="text">logged in as </span>' in output.data)
- self.assertEqual(output.data.count('Vote now!'), 1)
+ self.assertEqual(output.data.count('Vote now!'), 0)
def test_is_admin(self):
""" Test the is_admin function. """
@@ -304,7 +304,7 @@ class Flasktests(ModelFlasktests):
self.assertTrue('<h3>Next 3 elections</h3>' in output.data)
self.assertTrue(
'<span class="text">logged in as </span>' in output.data)
- self.assertEqual(output.data.count('Vote now!'), 1)
+ self.assertEqual(output.data.count('Vote now!'), 0)
if __name__ == '__main__':
commit 7ec3bd02e2f86989bee7df79f4e863f8c8ad877a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:04:23 2014 +0200
Add votes to the last election added in the tests suite
diff --git a/tests/test_vote.py b/tests/test_vote.py
index 09a4135..c3d81e5 100644
--- a/tests/test_vote.py
+++ b/tests/test_vote.py
@@ -164,6 +164,18 @@ class Votetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ # Election 6
+
+ obj = models.Vote( # id:1
+ election_id=6,
+ voter='toshio',
+ candidate_id=12,
+ value='1',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
def test_vote_count_with_votes(self):
""" Test the Candidate.vote_count function with votes in. """
self.test_init_vote()
commit 6b91d773dae1ec00637057c575f6d01d1786ecca
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:04:03 2014 +0200
Add candidates to the last election added
diff --git a/tests/test_candidate.py b/tests/test_candidate.py
index 3c0d6ba..aa87e95 100644
--- a/tests/test_candidate.py
+++ b/tests/test_candidate.py
@@ -165,6 +165,28 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ #
+ # Election #6
+ #
+
+ obj = models.Candidate( # id:12
+ election_id=6,
+ name='Toshio',
+ url='https://fedoraproject.org/wiki/User:Toshio',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Candidate( # id:13
+ election_id=6,
+ name='Kevin',
+ url='https://fedoraproject.org/wiki/User:Kevin',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
def test_to_json(self):
""" Test the Candidate.to_json function. """
self.test_init_candidate()
commit aeab747da63f0389d1369088ac02ff83152ccf52
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 15:26:53 2014 +0200
Adjust the unit-tests on the UI for the new election added
diff --git a/tests/test_flask.py b/tests/test_flask.py
index 6013d88..38c9961 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -95,7 +95,7 @@ class Flasktests(ModelFlasktests):
self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
self.assertTrue(
'<span class="text">logged in as </span>' in output.data)
- self.assertFalse('Vote now!' in output.data)
+ self.assertEqual(output.data.count('Vote now!'), 1)
def test_is_admin(self):
""" Test the is_admin function. """
@@ -280,7 +280,7 @@ class Flasktests(ModelFlasktests):
output = self.app.get('/open')
self.assertEqual(output.status_code, 200)
self.assertTrue('<title>Fedora elections</title>' in output.data)
- self.assertTrue('<h3>Next 2 elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 3 elections</h3>' in output.data)
self.assertTrue('<td>test election 3 shortdesc</td>' in output.data)
self.assertTrue('<a href="/vote/' in output.data)
self.assertTrue('<a href="/login">login</a>' in output.data)
@@ -290,21 +290,21 @@ class Flasktests(ModelFlasktests):
output = self.app.get('/open')
self.assertEqual(output.status_code, 200)
self.assertTrue('<title>Fedora elections</title>' in output.data)
- self.assertTrue('<h3>Next 2 elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 3 elections</h3>' in output.data)
self.assertTrue('<a href="/vote/' in output.data)
self.assertTrue(
'<span class="text">logged in as </span>' in output.data)
- self.assertTrue('Vote now!' in output.data)
+ self.assertEqual(output.data.count('Vote now!'), 3)
user = FakeUser([], username='toshio')
with user_set(fedora_elections.APP, user):
output = self.app.get('/open')
self.assertEqual(output.status_code, 200)
self.assertTrue('<title>Fedora elections</title>' in output.data)
- self.assertTrue('<h3>Next 2 elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 3 elections</h3>' in output.data)
self.assertTrue(
'<span class="text">logged in as </span>' in output.data)
- self.assertFalse('Vote now!' in output.data)
+ self.assertEqual(output.data.count('Vote now!'), 1)
if __name__ == '__main__':
commit f31265d0d013f6d6af521e471d633608c2e774d9
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 15:17:24 2014 +0200
Add a new election of select voting type and open
diff --git a/tests/test_election.py b/tests/test_election.py
index 342c253..90037ed 100644
--- a/tests/test_election.py
+++ b/tests/test_election.py
@@ -153,6 +153,24 @@ class Electiontests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ # Election - select voting - Open
+ obj = models.Election( # id:6
+ shortdesc='test election 6 shortdesc',
+ alias='test_election6',
+ description='test election 6 description',
+ url='https://fedoraproject.org',
+ start_date=TODAY - timedelta(days=1),
+ end_date=TODAY + timedelta(days=3),
+ seats_elected=1,
+ embargoed=1,
+ voting_type='select',
+ candidates_are_fasusers=0,
+ fas_user='skvidal',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
def test_get_election(self):
""" Test the Election.get function. """
self.test_init_election()
@@ -218,19 +236,21 @@ class Electiontests(Modeltests):
self.session, fas_user='skvidal')
self.assertNotEqual(obj, None)
self.assertNotEqual(obj, [])
- self.assertEqual(len(obj), 2)
+ self.assertEqual(len(obj), 3)
self.assertEqual(obj[0].description, 'test election 4 description')
self.assertEqual(obj[1].description, 'test election 5 description')
+ self.assertEqual(obj[2].description, 'test election 6 description')
obj = models.Election.search(self.session)
self.assertNotEqual(obj, None)
self.assertNotEqual(obj, [])
- self.assertEqual(len(obj), 5)
+ self.assertEqual(len(obj), 6)
self.assertEqual(obj[0].description, 'test election 4 description')
self.assertEqual(obj[1].description, 'test election 5 description')
- self.assertEqual(obj[2].description, 'test election 3 description')
- self.assertEqual(obj[3].description, 'test election 2 description')
- self.assertEqual(obj[4].description, 'test election description')
+ self.assertEqual(obj[2].description, 'test election 6 description')
+ self.assertEqual(obj[3].description, 'test election 3 description')
+ self.assertEqual(obj[4].description, 'test election 2 description')
+ self.assertEqual(obj[5].description, 'test election description')
def test_get_older_election(self):
""" Test the Election.get_older_election function. """
@@ -248,9 +268,10 @@ class Electiontests(Modeltests):
obj = models.Election.get_open_election(self.session, limit=TODAY)
self.assertNotEqual(obj, None)
self.assertNotEqual(obj, [])
- self.assertEqual(len(obj), 2)
+ self.assertEqual(len(obj), 3)
self.assertEqual(obj[0].shortdesc, 'test election 5 shortdesc')
- self.assertEqual(obj[1].shortdesc, 'test election 3 shortdesc')
+ self.assertEqual(obj[1].shortdesc, 'test election 6 shortdesc')
+ self.assertEqual(obj[2].shortdesc, 'test election 3 shortdesc')
def test_get_next_election(self):
""" Test the Election.get_next_election function. """
commit a02269ecca991a7e616b0062c688273e2820e69d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 15:09:49 2014 +0200
Move the simple voting unit-tests to their own file
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index b12353e..397db4a 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -166,206 +166,6 @@ class FlaskElectionstests(ModelFlasktests):
in output.data)
self.assertTrue('<h3>Current elections</h3>' in output.data)
- def test_vote_simple(self):
- """ Test the vote_simple function - the preview part. """
- output = self.app.get(
- '/vote/test_election', follow_redirects=True)
- self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data
- or 'discoveryfailure' in output.data)
-
- self.setup_db()
-
- user = FakeUser(['packager'], username='toshio')
- with user_set(fedora_elections.APP, user):
- output = self.app.get(
- '/vote/test_election5', follow_redirects=True)
- self.assertTrue(
- 'class="message">You have already voted in the election!</'
- in output.data)
-
- user = FakeUser(['packager'], username='pingou')
- with user_set(fedora_elections.APP, user):
- output = self.app.get(
- '/vote/test_election5')
- self.assertTrue(
- '<h2>test election 5 shortdesc</h2>' in output.data)
- self.assertTrue(
- '<input type="hidden" name="action" value="preview" />'
- in output.data)
-
- # Invalid vote: No candidate
- data = {
- 'action': 'preview',
- }
-
- output = self.app.post('/vote/test_election5', data=data)
- self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<h2>test election 5 shortdesc</h2>' in output.data)
- self.assertTrue(
- '<td class="error">Not a valid choice</td>'
- in output.data)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 1)
- self.assertTrue(
- '<input type="hidden" name="action" value="preview" />'
- in output.data)
-
- csrf_token = output.data.split(
- 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
-
- # Invalid vote: No candidate
- data = {
- 'action': 'preview',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post('/vote/test_election5', data=data)
- self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<h2>test election 5 shortdesc</h2>' in output.data)
- self.assertTrue(
- '<td class="error">Not a valid choice</td>'
- in output.data)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 1)
- self.assertTrue(
- '<input type="hidden" name="action" value="preview" />'
- in output.data)
-
- # Invalid vote: Not numeric
- data = {
- 'candidate': 'a',
- 'action': 'preview',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post('/vote/test_election5', data=data)
- self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<h2>test election 5 shortdesc</h2>' in output.data)
- self.assertTrue(
- '<input type="hidden" name="action" value="preview" />'
- in output.data)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 1)
-
- # Valid input
- data = {
- 'candidate': 7,
- 'action': 'preview',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post('/vote/test_election5', data=data)
- self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<h2>test election 5 shortdesc</h2>' in output.data)
- self.assertTrue(
- '<input type="hidden" name="action" value="submit" />'
- in output.data)
- self.assertTrue(
- '<li class="message">Please confirm your vote!</li>'
- in output.data)
-
- def test_vote_simple_process(self):
- """ Test the vote_simple function - the voting part. """
- output = self.app.get(
- '/vote/test_election', follow_redirects=True)
- self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data
- or 'discoveryfailure' in output.data)
-
- self.setup_db()
-
- user = FakeUser(['packager'], username='pingou')
- with user_set(fedora_elections.APP, user):
- # Invalid candidate id - no csrf
- data = {
- 'candidate': 1,
- 'action': 'submit',
- }
-
- output = self.app.post(
- '/vote/test_election5', data=data,
- follow_redirects=True)
- self.assertEqual(output.status_code, 200)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 1)
-
- csrf_token = output.data.split(
- 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
-
- # Invalid candidate id
- data = {
- 'candidate': 1,
- 'action': 'submit',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post(
- '/vote/test_election5', data=data,
- follow_redirects=True)
- self.assertEqual(output.status_code, 200)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 1)
-
- # Invalid vote: too low
- data = {
- 'candidate': -1,
- 'action': 'submit',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post(
- '/vote/test_election5', data=data,
- follow_redirects=True)
- self.assertEqual(output.status_code, 200)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 1)
-
- # Invalid vote: Not numeric
- data = {
- 'candidate': 'a',
- 'action': 'submit',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post(
- '/vote/test_election5', data=data,
- follow_redirects=True)
- self.assertEqual(output.status_code, 200)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 1)
-
- # Valid input
- data = {
- 'candidate': 8,
- 'action': 'submit',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post(
- '/vote/test_election5', data=data,
- follow_redirects=True)
- self.assertEqual(output.status_code, 200)
- self.assertTrue(
- 'class="message">Your vote has been recorded. Thank you!</'
- in output.data)
- self.assertTrue('<h3>Current elections</h3>' in output.data)
- self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
- self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
-
def test_election_results(self):
""" Test the election_results function - the preview part. """
output = self.app.get(
diff --git a/tests/test_flask_simple_voting.py b/tests/test_flask_simple_voting.py
new file mode 100644
index 0000000..aa28980
--- /dev/null
+++ b/tests/test_flask_simple_voting.py
@@ -0,0 +1,250 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections.elections test script
+"""
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import logging
+import unittest
+import sys
+import os
+
+from datetime import time
+from datetime import timedelta
+
+import flask
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+import fedora_elections
+from tests import ModelFlasktests, Modeltests, TODAY, FakeUser, user_set
+
+
+# pylint: disable=R0904
+class FlaskSimpleElectionstests(ModelFlasktests):
+ """ Flask application tests range voting. """
+
+ def test_vote_simple(self):
+ """ Test the vote_simple function - the preview part. """
+ output = self.app.get(
+ '/vote/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser(['packager'], username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/vote/test_election5', follow_redirects=True)
+ self.assertTrue(
+ 'class="message">You have already voted in the election!</'
+ in output.data)
+
+ user = FakeUser(['packager'], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/vote/test_election5')
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="hidden" name="action" value="preview" />'
+ in output.data)
+
+ # Invalid vote: No candidate
+ data = {
+ 'action': 'preview',
+ }
+
+ output = self.app.post('/vote/test_election5', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<td class="error">Not a valid choice</td>'
+ in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+ self.assertTrue(
+ '<input type="hidden" name="action" value="preview" />'
+ in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ # Invalid vote: No candidate
+ data = {
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/vote/test_election5', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<td class="error">Not a valid choice</td>'
+ in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+ self.assertTrue(
+ '<input type="hidden" name="action" value="preview" />'
+ in output.data)
+
+ # Invalid vote: Not numeric
+ data = {
+ 'candidate': 'a',
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/vote/test_election5', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="hidden" name="action" value="preview" />'
+ in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+
+ # Valid input
+ data = {
+ 'candidate': 7,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/vote/test_election5', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="hidden" name="action" value="submit" />'
+ in output.data)
+ self.assertTrue(
+ '<li class="message">Please confirm your vote!</li>'
+ in output.data)
+
+ def test_vote_simple_process(self):
+ """ Test the vote_simple function - the voting part. """
+ output = self.app.get(
+ '/vote/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser(['packager'], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ # Invalid candidate id - no csrf
+ data = {
+ 'candidate': 1,
+ 'action': 'submit',
+ }
+
+ output = self.app.post(
+ '/vote/test_election5', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ # Invalid candidate id
+ data = {
+ 'candidate': 1,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/vote/test_election5', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+
+ # Invalid vote: too low
+ data = {
+ 'candidate': -1,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/vote/test_election5', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+
+ # Invalid vote: Not numeric
+ data = {
+ 'candidate': 'a',
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/vote/test_election5', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+
+ # Valid input
+ data = {
+ 'candidate': 8,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/vote/test_election5', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ 'class="message">Your vote has been recorded. Thank you!</'
+ in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(FlaskSimpleElectionstests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit e0417f7b4ac8e5ff5d99a152092b3610db2dcccf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 15:07:54 2014 +0200
Move the voting range election unit-tests to their own file
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index abda30e..b12353e 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -166,244 +166,6 @@ class FlaskElectionstests(ModelFlasktests):
in output.data)
self.assertTrue('<h3>Current elections</h3>' in output.data)
- def test_vote_range(self):
- """ Test the vote_range function - the preview part. """
- output = self.app.get(
- '/vote/test_election', follow_redirects=True)
- self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data
- or 'discoveryfailure' in output.data)
-
- self.setup_db()
-
- user = FakeUser(['voters'], username='toshio')
- with user_set(fedora_elections.APP, user):
- output = self.app.get(
- '/vote/test_election3', follow_redirects=True)
- self.assertTrue(
- 'class="message">You have already voted in the election!</'
- in output.data)
-
- user = FakeUser(['voters'], username='pingou')
- with user_set(fedora_elections.APP, user):
- output = self.app.get(
- '/vote/test_election3')
- self.assertTrue(
- '<h2>test election 3 shortdesc</h2>' in output.data)
- self.assertTrue(
- '<input type="hidden" name="action" value="preview" />'
- in output.data)
-
- # Invalid candidate id
- data = {
- 'Toshio': 1,
- 'kevin': 3,
- 'Ralph': 2,
- 'action': 'preview',
- }
-
- output = self.app.post('/vote/test_election3', data=data)
- self.assertEqual(output.status_code, 200)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 3)
-
- csrf_token = output.data.split(
- 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
-
- # Invalid candidate id
- data = {
- 'Toshio': 1,
- 'Kevin': 3,
- 'Ralph': 2,
- 'action': 'preview',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post('/vote/test_election3', data=data)
- self.assertEqual(output.status_code, 200)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 3)
-
- # Invalid vote: too low
- data = {
- '9': -1,
- '6': 3,
- '5': 2,
- 'action': 'preview',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post('/vote/test_election3', data=data)
- self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<h2>test election 3 shortdesc</h2>' in output.data)
- self.assertTrue(
- '<input type="hidden" name="action" value="preview" />'
- in output.data)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 1)
-
- # Invalid vote: too high
- data = {
- '9': 5,
- '6': 3,
- '5': 2,
- 'action': 'preview',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post('/vote/test_election3', data=data)
- self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<h2>test election 3 shortdesc</h2>' in output.data)
- self.assertTrue(
- '<input type="hidden" name="action" value="preview" />'
- in output.data)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 1)
-
- # Invalid vote: Not numeric
- data = {
- '9': 'a',
- '6': 3,
- '5': 2,
- 'action': 'preview',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post('/vote/test_election3', data=data)
- self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<h2>test election 3 shortdesc</h2>' in output.data)
- self.assertTrue(
- '<input type="hidden" name="action" value="preview" />'
- in output.data)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 1)
-
- # Valid input
- data = {
- '4': 1,
- '6': 3,
- '5': 2,
- 'action': 'preview',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post('/vote/test_election3', data=data)
- self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<h2>test election 3 shortdesc</h2>' in output.data)
- self.assertTrue(
- '<input type="hidden" name="action" value="submit" />'
- in output.data)
- self.assertTrue('<div class="vtmedcool">' in output.data)
- self.assertTrue('<div class="vthot">' in output.data)
- self.assertTrue('<div class="vtmedwarm">' in output.data)
-
- def test_vote_range_process(self):
- """ Test the vote_range function - the voting part. """
- output = self.app.get(
- '/vote/test_election', follow_redirects=True)
- self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data
- or 'discoveryfailure' in output.data)
-
- self.setup_db()
-
- user = FakeUser(['voters'], username='pingou')
- with user_set(fedora_elections.APP, user):
- # No csrf token provided
- data = {
- 'Toshio': 1,
- 'Kevin': 3,
- 'Ralph': 2,
- 'action': 'submit',
- }
-
- output = self.app.post('/vote/test_election3', data=data)
- self.assertEqual(output.status_code, 200)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 3)
-
- csrf_token = output.data.split(
- 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
-
- # Invalid vote: too low
- data = {
- '4': -1,
- '5': 3,
- '6': 2,
- 'action': 'submit',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post(
- '/vote/test_election3', data=data)
- self.assertEqual(output.status_code, 200)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 1)
-
- # Invalid vote: too high
- data = {
- '4': 5,
- '5': 3,
- '6': 2,
- 'action': 'submit',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post(
- '/vote/test_election3', data=data, follow_redirects=True)
- self.assertEqual(output.status_code, 200)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 1)
-
- # Invalid vote: Not numeric
- data = {
- '4': 'a',
- '5': 3,
- '6': 2,
- 'action': 'submit',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post(
- '/vote/test_election3', data=data, follow_redirects=True)
- self.assertEqual(output.status_code, 200)
- self.assertEqual(
- output.data.count('<td class="error">Not a valid choice</td>'),
- 1)
-
- # Valid input
- data = {
- '4': 1,
- '5': 3,
- '6': 2,
- 'action': 'submit',
- 'csrf_token': csrf_token,
- }
-
- output = self.app.post(
- '/vote/test_election3', data=data, follow_redirects=True)
- self.assertEqual(output.status_code, 200)
- self.assertTrue(
- 'class="message">Your vote has been recorded. Thank you!</'
- in output.data)
- self.assertTrue('<h3>Current elections</h3>' in output.data)
- self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
- self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
-
def test_vote_simple(self):
""" Test the vote_simple function - the preview part. """
output = self.app.get(
diff --git a/tests/test_flask_range_voting.py b/tests/test_flask_range_voting.py
new file mode 100644
index 0000000..bf6cb95
--- /dev/null
+++ b/tests/test_flask_range_voting.py
@@ -0,0 +1,288 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections.elections test script
+"""
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import logging
+import unittest
+import sys
+import os
+
+from datetime import time
+from datetime import timedelta
+
+import flask
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+import fedora_elections
+from tests import ModelFlasktests, Modeltests, TODAY, FakeUser, user_set
+
+
+# pylint: disable=R0904
+class FlaskRangeElectionstests(ModelFlasktests):
+ """ Flask application tests range voting. """
+
+ def test_vote_range(self):
+ """ Test the vote_range function - the preview part. """
+ output = self.app.get(
+ '/vote/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser(['voters'], username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/vote/test_election3', follow_redirects=True)
+ self.assertTrue(
+ 'class="message">You have already voted in the election!</'
+ in output.data)
+
+ user = FakeUser(['voters'], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/vote/test_election3')
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="hidden" name="action" value="preview" />'
+ in output.data)
+
+ # Invalid candidate id
+ data = {
+ 'Toshio': 1,
+ 'kevin': 3,
+ 'Ralph': 2,
+ 'action': 'preview',
+ }
+
+ output = self.app.post('/vote/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 3)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ # Invalid candidate id
+ data = {
+ 'Toshio': 1,
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/vote/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 3)
+
+ # Invalid vote: too low
+ data = {
+ '9': -1,
+ '6': 3,
+ '5': 2,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/vote/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="hidden" name="action" value="preview" />'
+ in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+
+ # Invalid vote: too high
+ data = {
+ '9': 5,
+ '6': 3,
+ '5': 2,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/vote/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="hidden" name="action" value="preview" />'
+ in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+
+ # Invalid vote: Not numeric
+ data = {
+ '9': 'a',
+ '6': 3,
+ '5': 2,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/vote/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="hidden" name="action" value="preview" />'
+ in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+
+ # Valid input
+ data = {
+ '4': 1,
+ '6': 3,
+ '5': 2,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/vote/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="hidden" name="action" value="submit" />'
+ in output.data)
+ self.assertTrue('<div class="vtmedcool">' in output.data)
+ self.assertTrue('<div class="vthot">' in output.data)
+ self.assertTrue('<div class="vtmedwarm">' in output.data)
+
+ def test_vote_range_process(self):
+ """ Test the vote_range function - the voting part. """
+ output = self.app.get(
+ '/vote/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser(['voters'], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ # No csrf token provided
+ data = {
+ 'Toshio': 1,
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'submit',
+ }
+
+ output = self.app.post('/vote/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 3)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ # Invalid vote: too low
+ data = {
+ '4': -1,
+ '5': 3,
+ '6': 2,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/vote/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+
+ # Invalid vote: too high
+ data = {
+ '4': 5,
+ '5': 3,
+ '6': 2,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/vote/test_election3', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+
+ # Invalid vote: Not numeric
+ data = {
+ '4': 'a',
+ '5': 3,
+ '6': 2,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/vote/test_election3', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+
+ # Valid input
+ data = {
+ '4': 1,
+ '5': 3,
+ '6': 2,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/vote/test_election3', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ 'class="message">Your vote has been recorded. Thank you!</'
+ in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(FlaskRangeElectionstests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit 91f9f68a666aef47402a8b3d46221b87571bf6ec
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 13:05:33 2014 +0200
Adjust the unit-test now that we are using url_for() to build the links
diff --git a/tests/test_flask.py b/tests/test_flask.py
index f05a563..6013d88 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -258,8 +258,8 @@ class Flasktests(ModelFlasktests):
'<td>test election 2 shortdesc</td>' in output.data)
self.assertTrue(
'<td>test election shortdesc</td>' in output.data)
- self.assertEqual(output.data.count('<a href="results/'), 2)
- self.assertEqual(output.data.count('<a href="about/'), 2)
+ self.assertEqual(output.data.count('<a href="/results/'), 2)
+ self.assertEqual(output.data.count('<a href="/about/'), 2)
self.assertTrue('<a href="/login">login</a>' in output.data)
def test_open_elections(self):
commit d692f524e7aa0d43bf5ff1fc9ce101da606ffb3e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 13:05:13 2014 +0200
Fix the `More information` link to actually point to the about page
diff --git a/fedora_elections/templates/archive.html b/fedora_elections/templates/archive.html
index 38303d7..ea5d6db 100644
--- a/fedora_elections/templates/archive.html
+++ b/fedora_elections/templates/archive.html
@@ -18,7 +18,7 @@
<td>{{election.start_date}}</td>
<td>{{election.end_date}}</td>
<td>
- <a href="{{ url_for('vote', election_alias=election.alias) }}">
+ <a href="{{ url_for('about_election', election_alias=election.alias) }}">
More information
</a>
</td>
diff --git a/fedora_elections/templates/index.html b/fedora_elections/templates/index.html
index bda9b02..1381b41 100644
--- a/fedora_elections/templates/index.html
+++ b/fedora_elections/templates/index.html
@@ -22,7 +22,7 @@
<td>{{ election.end_date }}</td>
<td>{{ election.status }}</td>
<td>
- <a href="{{ url_for('vote', election_alias=election.alias) }}">
+ <a href="{{ url_for('about_election', election_alias=election.alias) }}">
More information
</a>
</td>
commit 1235e86a32cb9d71f12b5ebb8e9c48e225bacf24
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 12:49:06 2014 +0200
Use url_for() in the archive template to build the links
diff --git a/fedora_elections/templates/archive.html b/fedora_elections/templates/archive.html
index ec46f15..38303d7 100644
--- a/fedora_elections/templates/archive.html
+++ b/fedora_elections/templates/archive.html
@@ -18,10 +18,15 @@
<td>{{election.start_date}}</td>
<td>{{election.end_date}}</td>
<td>
- <a href="about/{{ election.alias }}">More information</a>
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">
+ More information
+ </a>
</td>
<td>
- <a href="results/{{ election.alias }}">Results</a>
+ <a href="{{ url_for('election_results',
+ election_alias=election.alias) }}">
+ Results
+ </a>
</td>
</tr>
{% endfor %}
commit d85529b324595b08190b15088fa890c1cc91ef73
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 12:48:45 2014 +0200
Add link to the about page on the index and open pages
diff --git a/fedora_elections/templates/index.html b/fedora_elections/templates/index.html
index 45518d4..bda9b02 100644
--- a/fedora_elections/templates/index.html
+++ b/fedora_elections/templates/index.html
@@ -13,6 +13,7 @@
<th>End date (UTC)</th>
<th>Status</th>
<th></th>
+ <th></th>
</tr>
{% for election in election_list %}
<tr>
@@ -21,6 +22,11 @@
<td>{{ election.end_date }}</td>
<td>{{ election.status }}</td>
<td>
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">
+ More information
+ </a>
+ </td>
+ <td>
{% if election.status == "In progress" %}
{% if election not in voted %}
<a href="{{ url_for('vote', election_alias=election.alias) }}">
commit 69180544fc1c6ad1d0e062c419cdf8e6a9c35f5f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 19:21:09 2014 +0200
Release 2.2
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 4af62f1..103cade 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -24,7 +24,7 @@
# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
-__version__ = '2.1.1'
+__version__ = '2.2'
import os
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index b4976a5..221b519 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -1,8 +1,8 @@
%define modname fedora_elections
Name: fedora-elections
-Version: 2.1.1
-Release: 2%{?dist}
+Version: 2.2
+Release: 1%{?dist}
Summary: Fedora elections application
Group: Development/Languages
@@ -109,6 +109,17 @@ install -m 644 files/update_1_to_2.sql \
%changelog
+* Thu Jul 10 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.2-1
+- Update to 2.2
+- Better description of the different types of election
+- Better error message from Chaoyi Zha
+- Fix FAS integration for range voting and simple voting
+- Update the logic to rely on candidate id for the field.short_name instead
+ of the candidate name as this was causing problem when the candidate name
+ had accents in it
+- Run the unit-tests against faitout only when running in jenkins
+- Adjust and improve the unit-tests suite
+
* Wed Jun 18 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.1.1-2
- Try running unit-tests in the %%check section
- Adjust the dependencies to include what's needed to run the tests
commit 35ed15d1808a0f546511de4d339e34aa7ae22474
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 19:10:55 2014 +0200
Expand the unit-tests to include a test w/ no max_votes set upon creation
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index d7963e1..2aa64a3 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -291,6 +291,46 @@ class FlaskAdmintests(ModelFlasktests):
self.assertTrue(
'<li>Legal voters: packager, testers</li>'
in output.data)
+ self.assertTrue(
+ '<li>Maximum number of votes: None</li>'
+ in output.data)
+
+ # All good - max_votes is ignored as it is not a integer
+ data = {
+ 'alias': 'new_election2',
+ 'shortdesc': 'new election2 shortdesc',
+ 'description': 'new election2 description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=2),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': 2,
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ 'admin_grp': 'testers, , sysadmin-main,,',
+ 'lgl_voters': 'testers, packager,,,',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/new', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Election "new_election2" added</li>'
+ in output.data)
+ self.assertTrue(
+ '<a href="/admin/new_election2/edit">' in output.data)
+ self.assertTrue(
+ '<p>There are no candidates.</p>' in output.data)
+ self.assertTrue(
+ '<li>Admin groups: sysadmin-main, testers</li>'
+ in output.data)
+ self.assertTrue(
+ '<li>Legal voters: packager, testers</li>'
+ in output.data)
+ self.assertTrue(
+ '<li>Maximum number of votes: None</li>'
+ in output.data)
def test_admin_edit_election(self):
""" Test the admin_edit_election function. """
commit e24ff00549904b69003068c4b84f651eb2b65f5e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 19:08:53 2014 +0200
Fix adding voter groups w/o spaces in their name
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index 1de6a3f..dd1abb0 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -110,7 +110,7 @@ def admin_new_election():
if voter_grp.strip():
lglvoters = models.LegalVoter(
election=election,
- group_name=voter_grp
+ group_name=voter_grp.strip(),
)
SESSION.add(lglvoters)
commit 908ba750f772a0d43cd156259033037a1d98b4f3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 19:03:53 2014 +0200
Expand the coverage to test for max_votes not being an int and adding lgl_voters groups when creating the election
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index 1f26757..d7963e1 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -257,7 +257,7 @@ class FlaskAdmintests(ModelFlasktests):
'<td class="error">There is already another election with '
'this summary.</td>' in output.data)
- # All good
+ # All good - max_votes is ignored as it is not a integer
data = {
'alias': 'new_election',
'shortdesc': 'new election shortdesc',
@@ -270,6 +270,8 @@ class FlaskAdmintests(ModelFlasktests):
'candidates_are_fasusers': False,
'embargoed': True,
'admin_grp': 'testers, sysadmin-main,,',
+ 'lgl_voters': 'testers, packager,,',
+ 'max_votes': 'wrong',
'csrf_token': csrf_token,
}
@@ -286,6 +288,9 @@ class FlaskAdmintests(ModelFlasktests):
self.assertTrue(
'<li>Admin groups: sysadmin-main, testers</li>'
in output.data)
+ self.assertTrue(
+ '<li>Legal voters: packager, testers</li>'
+ in output.data)
def test_admin_edit_election(self):
""" Test the admin_edit_election function. """
@@ -373,6 +378,7 @@ class FlaskAdmintests(ModelFlasktests):
'seats_elected': '2',
'candidates_are_fasusers': False,
'embargoed': False,
+ 'max_votes': 'wrong',
'csrf_token': csrf_token,
}
commit 24cd0e5b638ad39eb3d330a4d1a2fee1a5392718
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:56:57 2014 +0200
Adjust the unit-tests now that the range voting relies on candidate identifier rather than their name
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index d316ba7..abda30e 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -197,9 +197,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid candidate id
data = {
- '9': 1,
- '5': 3,
- '6': 2,
+ 'Toshio': 1,
+ 'kevin': 3,
+ 'Ralph': 2,
'action': 'preview',
}
@@ -214,9 +214,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid candidate id
data = {
- '9': 1,
- '5': 3,
- '6': 2,
+ 'Toshio': 1,
+ 'Kevin': 3,
+ 'Ralph': 2,
'action': 'preview',
'csrf_token': csrf_token,
}
@@ -229,9 +229,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid vote: too low
data = {
- 'Toshio': -1,
- 'Kevin': 3,
- 'Ralph': 2,
+ '9': -1,
+ '6': 3,
+ '5': 2,
'action': 'preview',
'csrf_token': csrf_token,
}
@@ -249,9 +249,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid vote: too high
data = {
- 'Toshio': 5,
- 'Kevin': 3,
- 'Ralph': 2,
+ '9': 5,
+ '6': 3,
+ '5': 2,
'action': 'preview',
'csrf_token': csrf_token,
}
@@ -269,9 +269,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid vote: Not numeric
data = {
- 'Toshio': 'a',
- 'Kevin': 3,
- 'Ralph': 2,
+ '9': 'a',
+ '6': 3,
+ '5': 2,
'action': 'preview',
'csrf_token': csrf_token,
}
@@ -289,9 +289,9 @@ class FlaskElectionstests(ModelFlasktests):
# Valid input
data = {
- 'Toshio': 1,
- 'Kevin': 3,
- 'Ralph': 2,
+ '4': 1,
+ '6': 3,
+ '5': 2,
'action': 'preview',
'csrf_token': csrf_token,
}
@@ -322,9 +322,9 @@ class FlaskElectionstests(ModelFlasktests):
with user_set(fedora_elections.APP, user):
# No csrf token provided
data = {
- '9': 1,
- '5': 3,
- '6': 2,
+ 'Toshio': 1,
+ 'Kevin': 3,
+ 'Ralph': 2,
'action': 'submit',
}
@@ -339,9 +339,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid vote: too low
data = {
- 'Toshio': -1,
- 'Kevin': 3,
- 'Ralph': 2,
+ '4': -1,
+ '5': 3,
+ '6': 2,
'action': 'submit',
'csrf_token': csrf_token,
}
@@ -355,9 +355,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid vote: too high
data = {
- 'Toshio': 5,
- 'Kevin': 3,
- 'Ralph': 2,
+ '4': 5,
+ '5': 3,
+ '6': 2,
'action': 'submit',
'csrf_token': csrf_token,
}
@@ -371,9 +371,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid vote: Not numeric
data = {
- 'Toshio': 'a',
- 'Kevin': 3,
- 'Ralph': 2,
+ '4': 'a',
+ '5': 3,
+ '6': 2,
'action': 'submit',
'csrf_token': csrf_token,
}
@@ -387,9 +387,9 @@ class FlaskElectionstests(ModelFlasktests):
# Valid input
data = {
- 'Toshio': 1,
- 'Kevin': 3,
- 'Ralph': 2,
+ '4': 1,
+ '5': 3,
+ '6': 2,
'action': 'submit',
'csrf_token': csrf_token,
}
commit 1df6483a53869846e74b95406a00bb83e5b64eb0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:56:26 2014 +0200
Adjust unit-tests for the archive page
diff --git a/tests/test_flask.py b/tests/test_flask.py
index 1ff7efe..f05a563 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -255,9 +255,9 @@ class Flasktests(ModelFlasktests):
self.assertTrue('<title>Fedora elections</title>' in output.data)
self.assertTrue('<h2>Past Elections</h2>' in output.data)
self.assertTrue(
- '<td>test election 2 description</td>' in output.data)
+ '<td>test election 2 shortdesc</td>' in output.data)
self.assertTrue(
- '<td>test election description</td>' in output.data)
+ '<td>test election shortdesc</td>' in output.data)
self.assertEqual(output.data.count('<a href="results/'), 2)
self.assertEqual(output.data.count('<a href="about/'), 2)
self.assertTrue('<a href="/login">login</a>' in output.data)
commit 5796530db4d7691f59331bece5c0452ce6fd3294
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:55:42 2014 +0200
Fix the range voting to be sure the user is not voting on a candidate of another election
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 6ae6793..9a3fcd7 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -129,9 +129,7 @@ def vote_range(election):
num_candidates = election.candidates.count()
- cand_name = {}
- for candidate in election.candidates:
- cand_name[candidate.name] = candidate.id
+ cand_ids = [str(cand.id) for cand in election.candidates]
next_action = 'confirm'
max_selection = num_candidates
@@ -149,11 +147,15 @@ def vote_range(election):
if candidate.short_name in ['csrf_token', 'action']:
continue
+ if candidate.short_name not in cand_ids:
+ flask.flash('Invalid input submitted', 'error')
+ return safe_redirect_back()
+
new_vote = models.Vote(
election_id=election.id,
voter=flask.g.fas_user.username,
timestamp=datetime.now(),
- candidate_id=cand_name[candidate.short_name],
+ candidate_id=candidate.short_name,
value=candidate.data,
)
SESSION.add(new_vote)
commit e7f4a9545378c32b53f37957c3208322835bef1e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:38:19 2014 +0200
Only run the unit-tests against faitout when the tests are ran in jenkins
diff --git a/tests/__init__.py b/tests/__init__.py
index 2881e5d..08c6833 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -50,14 +50,15 @@ from fedora_elections import models
DB_PATH = 'sqlite:///:memory:'
FAITOUT_URL = 'http://209.132.184.152/faitout/'
-try:
- import requests
- req = requests.get('%s/new' % FAITOUT_URL)
- if req.status_code == 200:
- DB_PATH = req.text
- print 'Using faitout at: %s' % DB_PATH
-except:
- pass
+if os.environ.get('BUILD_ID'):
+ try:
+ import requests
+ req = requests.get('%s/new' % FAITOUT_URL)
+ if req.status_code == 200:
+ DB_PATH = req.text
+ print 'Using faitout at: %s' % DB_PATH
+ except:
+ pass
TODAY = date.today()
commit f0f089217dbb6e23770477fd79fe562882dc54ef
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:37:51 2014 +0200
Fix the situation where the candidate.name has accent by using the candidate identifier
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index beee314..6ae6793 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -170,12 +170,12 @@ def vote_range(election):
if (election.candidates_are_fasusers): # pragma: no cover
for candidate in election.candidates:
try:
- usernamemap[candidate.name] = \
+ usernamemap[str(candidate.id)] = \
FAS2.person_by_username(candidate.name)['human_name']
except (KeyError, AuthError), err:
APP.logger.debug(err)
# User has their name set to private or user doesn't exist.
- usernamemap[candidate.name] = candidate.name
+ usernamemap[str(candidate.id)] = candidate.name
return flask.render_template(
'vote_range.html',
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 8dadd58..345056e 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -128,7 +128,7 @@ def get_range_voting_form(candidates, max_range):
title,
choices=[(str(item), item) for item in range(max_range + 1)]
)
- setattr(RangeVoting, candidate.name, field)
+ setattr(RangeVoting, str(candidate.id), field)
return RangeVoting()
commit 4a09461e316893c97b4571bd01601a4a196fe5a9
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:26:26 2014 +0200
For single vote election, do the FAS integration when building the form
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 6ab41e6..beee314 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -269,7 +269,8 @@ def vote_simple(election):
next_action = 'confirm'
form = forms.get_simple_voting_form(
- candidates=election.candidates)
+ candidates=election.candidates,
+ fasusers=election.candidates_are_fasusers)
if form.validate_on_submit():
if form.action.data == 'submit':
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 7d9d508..8dadd58 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -4,7 +4,8 @@ import flask
import wtforms
from flask.ext import wtf
-from fedora_elections import SESSION
+from fedora.client import AuthError
+from fedora_elections import SESSION, FAS2
from fedora_elections.models import Election
@@ -132,13 +133,19 @@ def get_range_voting_form(candidates, max_range):
return RangeVoting()
-def get_simple_voting_form(candidates):
+def get_simple_voting_form(candidates, fasusers):
class SimpleVoting(wtf.Form):
action = wtforms.HiddenField()
titles = []
for candidate in candidates:
title = candidate.name
+ if fasusers:
+ try:
+ title = \
+ FAS2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError), err:
+ APP.logger.debug(err)
if candidate.url:
title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
titles.append((str(candidate.id), title))
commit 105860efe9d50b0346fa9a7bf4ee38072a81f948
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:22:30 2014 +0200
Drop the usernamemap from the single vote election type
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 89fae07..6ab41e6 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -294,25 +294,11 @@ def vote_simple(election):
flask.flash("Please confirm your vote!")
next_action = 'vote'
- usernamemap = {}
- for candidate in election.candidates:
- if (election.candidates_are_fasusers): # pragma: no cover
- try:
- usernamemap[candidate.name] = \
- FAS2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError), err:
- APP.logger.debug(err)
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.name] = candidate.name
- else:
- usernamemap[candidate.name] = candidate.name
-
return flask.render_template(
'vote_simple.html',
election=election,
form=form,
num_candidates=num_candidates,
- usernamemap=usernamemap,
nextaction=next_action)
commit a6c1b672e76d36135d4a34c00429c0d7c0efa124
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:17:53 2014 +0200
Forward the usernamemap to the render_field_in_row macro to display the FAS user name correctly
diff --git a/fedora_elections/templates/vote_range.html b/fedora_elections/templates/vote_range.html
index 62bcc01..141259e 100644
--- a/fedora_elections/templates/vote_range.html
+++ b/fedora_elections/templates/vote_range.html
@@ -15,7 +15,7 @@
{{ render_field_data_in_row(
field, num_candidates=num_candidates, usernamemap=usernamemap) }}
{% else %}
- {{ render_field_in_row(field) }}
+ {{ render_field_in_row(field, usernamemap=usernamemap) }}
{% endif %}
{% endfor %}
commit 31e3aa70b2fa4988b4d9a2d6e6e9a0f27502b05c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:17:31 2014 +0200
Use the shortdesc rather than the description in the archive list/page
diff --git a/fedora_elections/templates/archive.html b/fedora_elections/templates/archive.html
index f0d539a..ec46f15 100644
--- a/fedora_elections/templates/archive.html
+++ b/fedora_elections/templates/archive.html
@@ -14,7 +14,7 @@
</tr>
{% for election in elections %}
<tr>
- <td>{{election.description}}</td>
+ <td>{{election.shortdesc}}</td>
<td>{{election.start_date}}</td>
<td>{{election.end_date}}</td>
<td>
commit 48ab15ec05cd79ef924cc6c7b974de4eced10cc3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:17:14 2014 +0200
Fix displaying the FAS name when the FAS integration is activated
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
index 4fea338..81c0abb 100644
--- a/fedora_elections/templates/_formhelpers.html
+++ b/fedora_elections/templates/_formhelpers.html
@@ -13,7 +13,7 @@
{% macro render_field_in_row(field, usernamemap=None, after="") %}
<tr>
- <td>{% if usernamema %} {{ usernamemap[field.label] }} {%
+ <td>{% if usernamemap %} {{ usernamemap[field.name] }} {%
else %} {{ field.label }} {% endif %}</td>
<td>{{ field(**kwargs)|safe }}</td>
{% if after %} <td>{{ after }}</td>{% endif %}
@@ -24,7 +24,7 @@
{% macro render_field_data_in_row(field, num_candidates, usernamemap=None, after="") %}
<tr>
- <td>{% if usernamema %} {{ usernamemap[field.label] }} {%
+ <td>{% if usernamemap %} {{ usernamemap[field.name] }} {%
else %} {{ field.label }} {% endif %}</td>
<td>
{% set weightpercent = (field.data | int *100) / num_candidates %}
commit 4c6d180471a75aea462e367d85dcf56a50e5f9e5
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:16:19 2014 +0200
Fix integrating candidate name with FAS when the election is set as such
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 1a6731c..89fae07 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -170,11 +170,12 @@ def vote_range(election):
if (election.candidates_are_fasusers): # pragma: no cover
for candidate in election.candidates:
try:
- usernamemap[candidate.id] = \
+ usernamemap[candidate.name] = \
FAS2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
+ except (KeyError, AuthError), err:
+ APP.logger.debug(err)
# User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
+ usernamemap[candidate.name] = candidate.name
return flask.render_template(
'vote_range.html',
@@ -242,11 +243,12 @@ def vote_select(election):
if (election.candidates_are_fasusers): # pragma: no cover
for candidate in election.candidates:
try:
- usernamemap[candidate.id] = \
+ usernamemap[candidate.name] = \
FAS2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
+ except (KeyError, AuthError), err:
+ APP.logger.debug(err)
# User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
+ usernamemap[candidate.name] = candidate.name
return flask.render_template(
'vote_simple.html',
@@ -298,7 +300,8 @@ def vote_simple(election):
try:
usernamemap[candidate.name] = \
FAS2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
+ except (KeyError, AuthError), err:
+ APP.logger.debug(err)
# User has their name set to private or user doesn't exist.
usernamemap[candidate.name] = candidate.name
else:
commit be46abe0c331bbe656495820deff0da1a7ce895b
Author: Chaoyi Zha <summermontreal(a)gmail.com>
Date: Sun Jun 22 07:32:44 2014 -0400
fix unit tests
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index d649f5e..d316ba7 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -672,8 +672,8 @@ class FlaskElectionstests(ModelFlasktests):
'/results/test_election3', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<li class="message">Sorry but this election is in progress, '
- 'you may not see its results already.</li>' in output.data)
+ '<li class="message">Sorry but this election is in progress,'
+ ' and you may not see its results yet.</li>' in output.data)
self.assertTrue('<h3>Current elections</h3>' in output.data)
commit 1994871e92a5289c531c3ad7f1eec65eff85ec23
Author: Chaoyi Zha <summermontreal(a)gmail.com>
Date: Sun Jun 22 07:17:31 2014 -0400
small typo
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index d774c7a..1a6731c 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -60,7 +60,7 @@ def login_required(f):
def get_valid_election(election_alias, ended=False):
- """ Return the election if it is valid (no pending, not ended).
+ """ Return the election if it is valid (not pending, not ended).
"""
election = models.Election.get(SESSION, alias=election_alias)
@@ -81,8 +81,8 @@ def get_valid_election(election_alias, ended=False):
elif ended and election.status == 'In progress':
flask.flash(
- 'Sorry but this election is in progress, you may not see its '
- 'results already.')
+ 'Sorry but this election is in progress, and you may not see its '
+ 'results yet.')
return safe_redirect_back()
return election
commit 408e3e94a2843e3e7638f54119f5184dc26a7f66
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sun Jun 22 10:28:36 2014 +0200
Prepare running the unit-tests at build time but cannot be activated for the moment
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index bd6ba44..b4976a5 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -2,7 +2,7 @@
Name: fedora-elections
Version: 2.1.1
-Release: 1%{?dist}
+Release: 2%{?dist}
Summary: Fedora elections application
Group: Development/Languages
@@ -22,6 +22,8 @@ BuildRequires: python-openid-teams
BuildRequires: python-openid-cla
BuildRequires: python-setuptools
BuildRequires: python-wtforms
+BuildRequires: python-nose
+BuildRequires: python-coverage
Requires: python-fedora
Requires: python-fedora-flask
@@ -89,11 +91,10 @@ cp -r alembic \
install -m 644 files/update_1_to_2.sql \
$RPM_BUILD_ROOT/%{_datadir}/fedora-elections/update_1_to_2.sql
-
-
-%clean
-rm -rf $RPM_BUILD_ROOT
-
+## Try running the unit-tests at build time but this requires flask 0.10+ which
+## is not present in elep6
+#%check
+#./runtests.sh -v -x
%files
%defattr(-,root,root,-)
@@ -108,6 +109,10 @@ rm -rf $RPM_BUILD_ROOT
%changelog
+* Wed Jun 18 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.1.1-2
+- Try running unit-tests in the %%check section
+- Adjust the dependencies to include what's needed to run the tests
+
* Tue Jun 10 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.1.1-1
- Update to 2.1.1
- Fix FAS integration (missing imports)
@@ -138,7 +143,7 @@ rm -rf $RPM_BUILD_ROOT
- Remove instanciated a sqlite database
- Install the sql upgrade script from elections v1 to elections v2
-* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 2-1
+* Tue Sep 03 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 2-1
- Simple voting
* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 2-0
commit 5d75608c4adbe7a93537cd570e880a1ab928f917
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sun Jun 22 10:26:47 2014 +0200
Include the runtest.sh and nosetests scripts in the releases
diff --git a/MANIFEST.in b/MANIFEST.in
index daf6963..c38b898 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,5 @@
include AUTHORS COPYING README requirements.txt
-include createdb.py
+include createdb.py runtests.sh nosetests
recursive-include fedora_elections *
recursive-include files *
recursive-include tests *
commit 4212f2df81357650291789953474a7c5949812b2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sun Jun 22 09:48:45 2014 +0200
Improve again the description of select voting
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 5b47c1f..7d9d508 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -34,7 +34,8 @@ class ElectionForm(wtf.Form):
('range', 'Range Voting'),
('simple', 'Simple Voting (choose one candidate in the list)'),
('range_3', 'Simplified Range Voting (max is set below)'),
- ('select', 'Select Voting (check or not the box, maximum vote set below)'),
+ ('select', 'Select Voting (checkboxes for each candidate, '
+ 'maximum number of votes set below)'),
],
default='range')
commit db9d859429121279a99f616076e9bb1d51f99490
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 18:43:26 2014 +0200
Improve the description of the different election types
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index bc13180..5b47c1f 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -32,9 +32,9 @@ class ElectionForm(wtf.Form):
'Type<span class="error">*</span>',
choices=[
('range', 'Range Voting'),
- ('simple', 'Simple Voting'),
- ('range_3', 'Simplified Range Voting (max = 3)'),
- ('select', 'Select Voting'),
+ ('simple', 'Simple Voting (choose one candidate in the list)'),
+ ('range_3', 'Simplified Range Voting (max is set below)'),
+ ('select', 'Select Voting (check or not the box, maximum vote set below)'),
],
default='range')
commit de24c84d3513c6605ddf8f984609b2b118ea46cf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 14:12:15 2014 +0200
Bump to 2.1.1
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 05ad731..4af62f1 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -24,7 +24,7 @@
# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
-__version__ = '2.1'
+__version__ = '2.1.1'
import os
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index f178c1d..bd6ba44 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -1,7 +1,7 @@
%define modname fedora_elections
Name: fedora-elections
-Version: 2.1
+Version: 2.1.1
Release: 1%{?dist}
Summary: Fedora elections application
@@ -108,6 +108,10 @@ rm -rf $RPM_BUILD_ROOT
%changelog
+* Tue Jun 10 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.1.1-1
+- Update to 2.1.1
+- Fix FAS integration (missing imports)
+
* Tue Jun 10 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.1-1
- Update to 2.1
- Add simplified range voting
commit 87f076f8bcc8906156adca659fd7e4c8ec42759f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 14:10:51 2014 +0200
Adjust the 1_to_2.sql file for 2.1+
diff --git a/files/update_1_to_2.sql b/files/update_1_to_2.sql
index 79bb69c..251886b 100644
--- a/files/update_1_to_2.sql
+++ b/files/update_1_to_2.sql
@@ -16,6 +16,10 @@ ALTER TABLE elections DROP COLUMN votes_per_user;
-- ALTER TABLE elections RENAME COLUMN shortdesc TO summary;
+
+-- For 2.1
ALTER TABLE elections DROP COLUMN candidates_are_fasusers;
ALTER TABLE elections RENAME COLUMN usefas TO candidates_are_fasusers;
+
+ALTER TABLE elections ALTER COLUMN candidates_are_fasusers TYPE integer;
commit e847389e4c5d11ff73b3ca9a8c0423ad9b5834bc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 14:10:37 2014 +0200
Fix missing imports
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 132304b..d774c7a 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -28,12 +28,13 @@ from datetime import datetime, time
from functools import wraps
import flask
+from fedora.client import AuthError
from fedora_elections import fedmsgshim
from fedora_elections import forms
from fedora_elections import models
from fedora_elections import (
- APP, SESSION, is_authenticated, is_admin, is_election_admin,
+ APP, SESSION, FAS2, is_authenticated, is_admin, is_election_admin,
is_safe_url, safe_redirect_back,
)
commit d1a2564480460fd821b704313d333cc954c4394a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 13:00:41 2014 +0200
Bump to 2.1
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index abb74a4..05ad731 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -24,7 +24,7 @@
# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
-__version__ = '2.0.2'
+__version__ = '2.1'
import os
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index fb45c54..f178c1d 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -1,7 +1,7 @@
%define modname fedora_elections
Name: fedora-elections
-Version: 2.0.2
+Version: 2.1
Release: 1%{?dist}
Summary: Fedora elections application
@@ -108,6 +108,19 @@ rm -rf $RPM_BUILD_ROOT
%changelog
+* Tue Jun 10 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.1-1
+- Update to 2.1
+- Add simplified range voting
+- Add select voting
+- Add group admins
+- Add legal voters
+- Add csrf protections to the voting page
+- Decorate the result page to actually show the result/selection
+- Put all the votes at the same URL
+- Fix using https everywhere
+- fedmsg integration upon election/candidate creation, edition, deletion
+- unit-tests
+
* Sat May 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.0.2-1
- Update to 2.0.2
- Fix is_safe_url method, imports were different here
commit 5031994c546dc3a03b5a606a0dba4ae47e21f6b6
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 13:04:46 2014 +0200
Install the sample/default alembic.ini file as no-replace
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index 366cdbb..fb45c54 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -77,6 +77,11 @@ install -m 644 files/fedora-elections.wsgi \
install -m 644 createdb.py \
$RPM_BUILD_ROOT/%{_datadir}/fedora-elections/fedora-elections_createdb.py
+# Install the alembic configuration file
+mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/fedora-elections
+install -m 644 files/alembic.ini \
+ $RPM_BUILD_ROOT/%{_sysconfdir}/fedora-elections/alembic.ini
+
# Install the alembic files
cp -r alembic \
$RPM_BUILD_ROOT/%{_datadir}/fedora-elections/alembic
@@ -95,6 +100,7 @@ rm -rf $RPM_BUILD_ROOT
%doc
%dir %{_sysconfdir}/%{name}
%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
+%config(noreplace) %{_sysconfdir}/%{name}/alembic.ini
%config(noreplace) %{_sysconfdir}/httpd/conf.d/%{name}.conf
%{python_sitelib}/%{modname}/
%{python_sitelib}/%{modname}*.egg-info/
commit 07ca32cb8ad45ce722e5f61181a0df4b03e1dc9d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 13:03:56 2014 +0200
Install the alembic files in the spec
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index f3598a7..366cdbb 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -77,6 +77,10 @@ install -m 644 files/fedora-elections.wsgi \
install -m 644 createdb.py \
$RPM_BUILD_ROOT/%{_datadir}/fedora-elections/fedora-elections_createdb.py
+# Install the alembic files
+cp -r alembic \
+ $RPM_BUILD_ROOT/%{_datadir}/fedora-elections/alembic
+
install -m 644 files/update_1_to_2.sql \
$RPM_BUILD_ROOT/%{_datadir}/fedora-elections/update_1_to_2.sql
commit 005dfb3b057106a054f88d82799fea8fb22d9f6b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 13:03:04 2014 +0200
Include the alembic files in the sources
diff --git a/MANIFEST.in b/MANIFEST.in
index c6b69a5..daf6963 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,3 +3,4 @@ include createdb.py
recursive-include fedora_elections *
recursive-include files *
recursive-include tests *
+recursive-include alembic *
commit 509bd1fdfe65c1f6519d4d910f07950c605d2b03
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 31 12:55:07 2014 +0200
Take into account max_votes when editing an election
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index af03f95..1de6a3f 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -156,6 +156,14 @@ def admin_edit_election(election_alias):
form = forms.ElectionForm(election.id, obj=election)
if form.validate_on_submit():
form.embargoed.data = int(form.embargoed.data)
+ if form.max_votes.data:
+ try:
+ form.max_votes.data = int(form.max_votes.data)
+ except ValueError:
+ form.max_votes.data = None
+ else:
+ form.max_votes.data = None
+
form.candidates_are_fasusers.data = int(
form.candidates_are_fasusers.data)
form.populate_obj(election)
commit 7b63a48d9e8af4566ac400cbe449240d5c03d180
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 31 12:54:17 2014 +0200
Take the max_votes into account when creating an election
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index 32a4ee6..af03f95 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -67,6 +67,14 @@ def admin_new_election():
form = forms.ElectionForm()
if form.validate_on_submit():
+ if form.max_votes.data:
+ try:
+ form.max_votes.data = int(form.max_votes.data)
+ except ValueError:
+ form.max_votes.data = None
+ else:
+ form.max_votes.data = None
+
election = models.Election(
shortdesc=form.shortdesc.data,
alias=form.alias.data,
@@ -77,6 +85,7 @@ def admin_new_election():
seats_elected=form.seats_elected.data,
embargoed=int(form.embargoed.data),
voting_type=form.voting_type.data,
+ max_votes=form.max_votes.data,
candidates_are_fasusers=int(form.candidates_are_fasusers.data),
fas_user=flask.g.fas_user.username,
)
commit ec40adad5c44daff5471f409ce5eb0cb4413f072
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 18:20:51 2014 +0200
Small pep8 fixes
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index e6726f4..132304b 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -99,8 +99,8 @@ def vote(election_alias):
if len(set(flask.g.fas_user.groups).intersection(
set(election.legal_voters_list))) == 0:
flask.flash(
- 'You are not among the groups that are allowed to vote for this '
- 'election', 'error')
+ 'You are not among the groups that are allowed to vote '
+ 'for this election', 'error')
return safe_redirect_back()
votes = models.Vote.of_user_on_election(
@@ -257,7 +257,6 @@ def vote_select(election):
nextaction=next_action)
-
def vote_simple(election):
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index 5f0402a..1f26757 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -390,7 +390,8 @@ class FlaskAdmintests(ModelFlasktests):
in output.data)
def test_admin_edit_election_admin_groups(self):
- """ Test the admin_edit_election function when editing admin groups. """
+ """ Test the admin_edit_election function when editing admin groups.
+ """
user = FakeUser(
fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
username='toshio')
@@ -474,7 +475,8 @@ class FlaskAdmintests(ModelFlasktests):
in output.data)
def test_admin_edit_election_legal_voters(self):
- """ Test the admin_edit_election function when editing legal voters. """
+ """ Test the admin_edit_election function when editing legal voters.
+ """
user = FakeUser(
fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
username='toshio')
commit dc5ccdfb26da900ece3150759a2cd11b9dcf43cb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:35:50 2014 +0200
Add alembic revision adding max_votes to the elections table
diff --git a/alembic/versions/d07c5ef2d03_add_max_votes_to_ele.py b/alembic/versions/d07c5ef2d03_add_max_votes_to_ele.py
new file mode 100644
index 0000000..4a922ff
--- /dev/null
+++ b/alembic/versions/d07c5ef2d03_add_max_votes_to_ele.py
@@ -0,0 +1,28 @@
+"""Add max_votes to election
+
+
+Revision ID: d07c5ef2d03
+Revises: None
+Create Date: 2014-05-26 16:33:50.812050
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = 'd07c5ef2d03'
+down_revision = None
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ """ Add the max_votes column to the Elections table. """
+ op.add_column(
+ 'elections',
+ sa.Column('max_votes', sa.Integer, nullable=True)
+ )
+
+
+def downgrade():
+ """ Drop the max_votes column from the Elections table. """
+ op.drop_column('elections', 'max_votes')
commit d66dfb962ffb48f61bcaf82894c3044c00a56c76
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:36:18 2014 +0200
Ignore alembic.ini files
diff --git a/.gitignore b/.gitignore
index fc2dc27..3ea936c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@
build/
dist/
.coverage
+alembic.ini
commit e3d8d37dff15aa7911bf4b7c5c4787e5ead72a1b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:35:38 2014 +0200
Add sample alembic.ini file in the files folder
diff --git a/files/alembic.ini b/files/alembic.ini
new file mode 100644
index 0000000..920f5df
--- /dev/null
+++ b/files/alembic.ini
@@ -0,0 +1,54 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+
+# In production
+# script_location = /usr/share/fedora_elections/alembic
+# For development
+script_location = alembic
+
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+sqlalchemy.url = driver://user:pass@localhost/dbname
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
commit 8d9dafefb4fddfa90dade8cb267735fd5aa45ade
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:35:22 2014 +0200
Add basic alembic structure for alembic integration
diff --git a/alembic/env.py b/alembic/env.py
new file mode 100644
index 0000000..a280347
--- /dev/null
+++ b/alembic/env.py
@@ -0,0 +1,72 @@
+from __future__ import with_statement
+from alembic import context
+from sqlalchemy import engine_from_config, pool
+from logging.config import fileConfig
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+target_metadata = None
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(url=url)
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(
+ config.get_section(config.config_ini_section),
+ prefix='sqlalchemy.',
+ poolclass=pool.NullPool)
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata)
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/alembic/script.py.mako b/alembic/script.py.mako
new file mode 100644
index 0000000..9570201
--- /dev/null
+++ b/alembic/script.py.mako
@@ -0,0 +1,22 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision}
+Create Date: ${create_date}
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
commit 3aa17087b1dc9ed982a8fc4714f58253c9ac0c5c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:30:27 2014 +0200
The simple voting form has no use of the maximum votes/range
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 411f609..e6726f4 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -267,8 +267,7 @@ def vote_simple(election):
next_action = 'confirm'
form = forms.get_simple_voting_form(
- candidates=election.candidates,
- max_range=num_candidates)
+ candidates=election.candidates)
if form.validate_on_submit():
if form.action.data == 'submit':
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index c655939..bc13180 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -131,7 +131,7 @@ def get_range_voting_form(candidates, max_range):
return RangeVoting()
-def get_simple_voting_form(candidates, max_range):
+def get_simple_voting_form(candidates):
class SimpleVoting(wtf.Form):
action = wtforms.HiddenField()
commit d75e6b95fa6be67194645d39b67bff20de597fed
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:27:27 2014 +0200
Use the max_votes field of the election to set the maximum ranges or vote
This in the range_voting and select_voting type of elections
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index e3549ad..411f609 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -112,8 +112,6 @@ def vote(election_alias):
if election.voting_type == 'range':
return vote_range(election)
- elif election.voting_type == 'range_3':
- return vote_range(election, max_range=3)
elif election.voting_type == 'simple':
return vote_simple(election)
elif election.voting_type == 'select':
@@ -124,7 +122,7 @@ def vote(election_alias):
return safe_redirect_back()
-def vote_range(election, max_range=None):
+def vote_range(election):
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
@@ -135,12 +133,13 @@ def vote_range(election, max_range=None):
cand_name[candidate.name] = candidate.id
next_action = 'confirm'
- if max_range is None:
- max_range = num_candidates
+ max_selection = num_candidates
+ if election.max_votes:
+ max_selection = election.max_votes
form = forms.get_range_voting_form(
candidates=election.candidates,
- max_range=max_range)
+ max_range=max_selection)
if form.validate_on_submit():
@@ -181,12 +180,12 @@ def vote_range(election, max_range=None):
election=election,
form=form,
num_candidates=num_candidates,
- max_range=max_range,
+ max_range=max_selection,
usernamemap=usernamemap,
nextaction=next_action)
-def vote_select(election, max_selection=None):
+def vote_select(election):
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
@@ -197,8 +196,9 @@ def vote_select(election, max_selection=None):
cand_name[candidate.name] = candidate.id
next_action = 'confirm'
- if max_selection is None:
- max_selection = num_candidates
+ max_selection = num_candidates
+ if election.max_votes:
+ max_selection = election.max_votes
form = forms.get_select_voting_form(
candidates=election.candidates,
@@ -206,27 +206,36 @@ def vote_select(election, max_selection=None):
if form.validate_on_submit():
- if form.action.data == 'submit':
- for candidate in form:
- if candidate.short_name in ['csrf_token', 'action']:
- continue
-
- new_vote = models.Vote(
- election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=cand_name[candidate.short_name],
- value=int(candidate.data),
- )
- SESSION.add(new_vote)
- SESSION.commit()
-
- flask.flash("Your vote has been recorded. Thank you!")
- return safe_redirect_back()
+ cnt = 0
+ for candidate in form:
+ if candidate.short_name in ['csrf_token', 'action']:
+ continue
+ if candidate.data:
+ cnt += 1
+ if cnt > max_selection:
+ flask.flash('Too many candidates submitted', 'error')
+ else:
+ if form.action.data == 'submit':
+ for candidate in form:
+ if candidate.short_name in ['csrf_token', 'action']:
+ continue
+
+ new_vote = models.Vote(
+ election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=cand_name[candidate.short_name],
+ value=int(candidate.data),
+ )
+ SESSION.add(new_vote)
+ SESSION.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return safe_redirect_back()
- if form.action.data == 'preview':
- flask.flash("Please confirm your vote!")
- next_action = 'vote'
+ if form.action.data == 'preview':
+ flask.flash("Please confirm your vote!")
+ next_action = 'vote'
usernamemap = {}
if (election.candidates_are_fasusers): # pragma: no cover
commit 0539a49cb77b19f9415896af23b8424171f2242c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:28:04 2014 +0200
Adjust the edit and view election templates to display the max_votes field/info
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index 050c94d..fceb75a 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -15,6 +15,7 @@
{{ render_field_in_row(form.shortdesc) }}
{{ render_field_in_row(form.description) }}
{{ render_field_in_row(form.voting_type) }}
+ {{ render_field_in_row(form.max_votes) }}
{{ render_field_in_row(form.url) }}
{{ render_field_in_row(form.start_date, " at 00:00:00 UTC") }}
{{ render_field_in_row(form.end_date, " at 23:59:59 UTC") }}
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index 90ab10b..2247754 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -15,6 +15,7 @@
<li>End date: {{ election.end_date }}</li>
<li>Number elected: {{ election.seats_elected }}</li>
<li>Election type: {{ election.voting_type }}</li>
+ <li>Maximum number of votes: {{ election.max_votes }}</li>
<li>Admin groups: {{ election.admin_groups_list | join(', ') }}</li>
<li>Legal voters: {{ election.legal_voters_list | join(', ') }}</li>
</ul>
commit 9759dd604b3ce3734df90dd2e865a94d7180dfaf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:24:24 2014 +0200
Add the max_votes field in the election form
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index dbe01cb..c655939 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -38,6 +38,10 @@ class ElectionForm(wtf.Form):
],
default='range')
+ max_votes = wtforms.TextField(
+ 'Maximum Range/Votes',
+ [wtforms.validators.optional()])
+
url = wtforms.TextField(
'URL<span class="error">*</span>', [
wtforms.validators.Required(),
commit f48421957d6bf2775f7cea73550865a5d5836186
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:24:13 2014 +0200
Add a max_votes field in the election table
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index e125545..e0e5941 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -94,6 +94,7 @@ class Election(BASE):
seats_elected = sa.Column(sa.Integer, nullable=False, default=1)
embargoed = sa.Column(sa.Integer, nullable=False, default=0)
voting_type = sa.Column(sa.Unicode(100), nullable=False, default=u'range')
+ max_votes = sa.Column(sa.Integer, nullable=True)
candidates_are_fasusers = sa.Column(
sa.Integer, nullable=False, default=0)
fas_user = sa.Column(sa.Unicode(50), nullable=False)
commit ba5a54e7fd013ae01a3577cc86e1565d48f771cd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:14:39 2014 +0200
Add the logic for vote select
For this type of vote, the user can simply select the candidates of
interest eventually with a maximum number of choices.
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index b4ebd0e..e3549ad 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -116,6 +116,8 @@ def vote(election_alias):
return vote_range(election, max_range=3)
elif election.voting_type == 'simple':
return vote_simple(election)
+ elif election.voting_type == 'select':
+ return vote_select(election)
else: # pragma: no cover
flask.flash(
'Unknown election voting type: %s' % election.voting_type)
@@ -184,6 +186,69 @@ def vote_range(election, max_range=None):
nextaction=next_action)
+def vote_select(election, max_selection=None):
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, election.id, count=True)
+
+ num_candidates = election.candidates.count()
+
+ cand_name = {}
+ for candidate in election.candidates:
+ cand_name[candidate.name] = candidate.id
+ next_action = 'confirm'
+
+ if max_selection is None:
+ max_selection = num_candidates
+
+ form = forms.get_select_voting_form(
+ candidates=election.candidates,
+ max_selection=max_selection)
+
+ if form.validate_on_submit():
+
+ if form.action.data == 'submit':
+ for candidate in form:
+ if candidate.short_name in ['csrf_token', 'action']:
+ continue
+
+ new_vote = models.Vote(
+ election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=cand_name[candidate.short_name],
+ value=int(candidate.data),
+ )
+ SESSION.add(new_vote)
+ SESSION.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return safe_redirect_back()
+
+ if form.action.data == 'preview':
+ flask.flash("Please confirm your vote!")
+ next_action = 'vote'
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers): # pragma: no cover
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ FAS2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template(
+ 'vote_simple.html',
+ election=election,
+ form=form,
+ num_candidates=num_candidates,
+ max_selection=max_selection,
+ usernamemap=usernamemap,
+ nextaction=next_action)
+
+
+
def vote_simple(election):
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
commit 168209cb9a4a561a9dfec7dfb32dd431a05192d3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:14:17 2014 +0200
Add select voting as an option when creating an election
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 62b8dd1..dbe01cb 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -34,6 +34,7 @@ class ElectionForm(wtf.Form):
('range', 'Range Voting'),
('simple', 'Simple Voting'),
('range_3', 'Simplified Range Voting (max = 3)'),
+ ('select', 'Select Voting'),
],
default='range')
commit 08254717559b51c1dcdba47b01768a6be662b487
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:10:48 2014 +0200
Add a form for SelectVoting
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 1abedca..62b8dd1 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -143,3 +143,21 @@ def get_simple_voting_form(candidates, max_range):
setattr(SimpleVoting, 'candidate', field)
return SimpleVoting()
+
+
+def get_select_voting_form(candidates, max_selection):
+ class SelectVoting(wtf.Form):
+ action = wtforms.HiddenField()
+
+ for candidate in candidates:
+ title = candidate.name
+ if candidate.url:
+ title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
+ field = wtforms.BooleanField(
+ title,
+ )
+ setattr(SelectVoting, candidate.name, field)
+
+ # TODO: See if we can add a form wide validation using max_selection
+
+ return SelectVoting()
commit af5931aed99830bb3c4f14aed3c6622cd5df9edc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 15:36:34 2014 +0200
If the election is of type range_3, call the appropriately vote_range
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 206cf7a..b4ebd0e 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -112,6 +112,8 @@ def vote(election_alias):
if election.voting_type == 'range':
return vote_range(election)
+ elif election.voting_type == 'range_3':
+ return vote_range(election, max_range=3)
elif election.voting_type == 'simple':
return vote_simple(election)
else: # pragma: no cover
commit 8a4311e01351b44945a538ebd4dc75a3d4040699
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 15:35:54 2014 +0200
Offer the simplified range voting (max range = 3) in the options when creating an election
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index c58b5ff..1abedca 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -30,8 +30,11 @@ class ElectionForm(wtf.Form):
voting_type = wtforms.RadioField(
'Type<span class="error">*</span>',
- choices=[('range', 'Range Voting'),
- ('simple', 'Simple Voting')],
+ choices=[
+ ('range', 'Range Voting'),
+ ('simple', 'Simple Voting'),
+ ('range_3', 'Simplified Range Voting (max = 3)'),
+ ],
default='range')
url = wtforms.TextField(
commit af76d0f259730180963d971ec987aaab7307f60d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 15:35:25 2014 +0200
Update the information message according to the type of voting used for this election
diff --git a/fedora_elections/templates/vote_range.html b/fedora_elections/templates/vote_range.html
index e1a003a..62bcc01 100644
--- a/fedora_elections/templates/vote_range.html
+++ b/fedora_elections/templates/vote_range.html
@@ -48,6 +48,8 @@
{% else %}
<p />
{% endif %}
+
+{% if election.voting_type == 'range' %}
<div>
<h2>Information on Range Voting</h2>
<p>
@@ -58,8 +60,31 @@
</p>
<p>
To cast your vote in this election simply select a value between 0 and
- {{num_candidates}} with 0 as 'least or no preference' and
- {{num_candidates}} as 'highest preference'.
+ {{ max_range }} with 0 as 'least or no preference' and
+ {{ max_range }} as 'highest preference'.
+ </p>
+ <p>
+ At the end of the election, the highest ranking candidate(s) are marked
+ as the winners.
+ </p>
+ <p>
+ For more information about Range Voting, visit the
+ <a href="http://rangevoting.org/">Center for Range Voting</a>.
+ </p>
+</div>
+{% elif election.voting_type == 'range_3' %}
+<div>
+ <h2>Information on Simplified Range Voting</h2>
+ <p>
+ Fedora Project has implemented a simplified version of
+ <a href="http://rangevoting.org/">Range Voting</a>
+ for this election, in particular the "Range (score-summing, blanks
+ treated as zero score, no quorum rule)" range voting system.
+ </p>
+ <p>
+ To cast your vote in this election simply select a value between 0 and
+ {{ max_range }} with 0 as 'least or no preference' and
+ {{ max_range }} as 'highest preference'.
</p>
<p>
At the end of the election, the highest ranking candidate(s) are marked
@@ -70,4 +95,5 @@
<a href="http://rangevoting.org/">Center for Range Voting</a>.
</p>
</div>
+{% endif %}
{% endblock %}
commit f40500e996937843800211092575f2ea154ec70f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 15:34:00 2014 +0200
Have the vote_range function accept a max_range argument
This argument is the maximal value the user can give to its vote.
If none is provided, it defaults to the number of candidates which is the
default range voting mechanism
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 9e06957..206cf7a 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -120,7 +120,7 @@ def vote(election_alias):
return safe_redirect_back()
-def vote_range(election):
+def vote_range(election, max_range=None):
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
@@ -131,9 +131,12 @@ def vote_range(election):
cand_name[candidate.name] = candidate.id
next_action = 'confirm'
+ if max_range is None:
+ max_range = num_candidates
+
form = forms.get_range_voting_form(
candidates=election.candidates,
- max_range=num_candidates)
+ max_range=max_range)
if form.validate_on_submit():
@@ -174,6 +177,7 @@ def vote_range(election):
election=election,
form=form,
num_candidates=num_candidates,
+ max_range=max_range,
usernamemap=usernamemap,
nextaction=next_action)
commit e7a015294a3347da0400d8364bcdfbfd75d7b86f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:11:49 2014 +0200
Simple voting directly use the candidate identifier now, adjust the tests accordingly
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index 3478b44..d649f5e 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -495,7 +495,7 @@ class FlaskElectionstests(ModelFlasktests):
# Valid input
data = {
- 'candidate': 'Ralph',
+ 'candidate': 7,
'action': 'preview',
'csrf_token': csrf_token,
}
@@ -588,7 +588,7 @@ class FlaskElectionstests(ModelFlasktests):
# Valid input
data = {
- 'candidate': 'Toshio',
+ 'candidate': 8,
'action': 'submit',
'csrf_token': csrf_token,
}
commit 17a06ea0e636ed52c898934909870c340cdaed06
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:09:54 2014 +0200
The usernamemap converts the field label (username) for FAS full names
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index d0e1459..9e06957 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -217,13 +217,13 @@ def vote_simple(election):
for candidate in election.candidates:
if (election.candidates_are_fasusers): # pragma: no cover
try:
- usernamemap[candidate.id] = \
+ usernamemap[candidate.name] = \
FAS2.person_by_username(candidate.name)['human_name']
except (KeyError, AuthError):
# User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
+ usernamemap[candidate.name] = candidate.name
else:
- usernamemap[candidate.id] = candidate.name
+ usernamemap[candidate.name] = candidate.name
return flask.render_template(
'vote_simple.html',
commit 769901de8f54c20a0fa292dfb5443a32b7a5efb2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:09:39 2014 +0200
Make use the usernamemap for range voting
diff --git a/fedora_elections/templates/vote_range.html b/fedora_elections/templates/vote_range.html
index 5e0ff04..e1a003a 100644
--- a/fedora_elections/templates/vote_range.html
+++ b/fedora_elections/templates/vote_range.html
@@ -12,7 +12,8 @@
<table border="1" cellpadding="1">
{% for field in form if field.widget.input_type != 'hidden' %}
{% if nextaction == 'vote' %}
- {{ render_field_data_in_row(field, num_candidates=num_candidates) }}
+ {{ render_field_data_in_row(
+ field, num_candidates=num_candidates, usernamemap=usernamemap) }}
{% else %}
{{ render_field_in_row(field) }}
{% endif %}
commit 44b9c492597fb3e6bf1045c301f9e547aed187be
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:08:34 2014 +0200
Allow to rely on the usernamemap also in render_field_in_row
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
index 24df810..4fea338 100644
--- a/fedora_elections/templates/_formhelpers.html
+++ b/fedora_elections/templates/_formhelpers.html
@@ -11,9 +11,10 @@
</div>
{% endmacro %}
-{% macro render_field_in_row(field, after="") %}
+{% macro render_field_in_row(field, usernamemap=None, after="") %}
<tr>
- <td>{{ field.label }}</td>
+ <td>{% if usernamema %} {{ usernamemap[field.label] }} {%
+ else %} {{ field.label }} {% endif %}</td>
<td>{{ field(**kwargs)|safe }}</td>
{% if after %} <td>{{ after }}</td>{% endif %}
{% if field.errors %}{% for error in field.errors
commit 449256a2b6ba68ec2c4eaa266181577352fb928f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:06:55 2014 +0200
The form now provides the candidate id directly as value (instead of its name before)
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 2c29955..d0e1459 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -200,7 +200,7 @@ def vote_simple(election):
election_id=election.id,
voter=flask.g.fas_user.username,
timestamp=datetime.now(),
- candidate_id=cand_name[candidate.data],
+ candidate_id=candidate.data,
value=1,
)
SESSION.add(new_vote)
commit 0333f0ab84339365fc01219d79c9b522d5f59f64
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:06:34 2014 +0200
Fill in the usernamemap for all the candidates
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index a00201e..2c29955 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -214,14 +214,16 @@ def vote_simple(election):
next_action = 'vote'
usernamemap = {}
- if (election.candidates_are_fasusers): # pragma: no cover
- for candidate in election.candidates:
+ for candidate in election.candidates:
+ if (election.candidates_are_fasusers): # pragma: no cover
try:
usernamemap[candidate.id] = \
FAS2.person_by_username(candidate.name)['human_name']
except (KeyError, AuthError):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
+ else:
+ usernamemap[candidate.id] = candidate.name
return flask.render_template(
'vote_simple.html',
commit ae4093e925848574bef54895a6e5feed8af6fa8d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:06:06 2014 +0200
Drop the cand_name dict as we already have the usernamemap dict
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 3cf40e3..a00201e 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -184,9 +184,6 @@ def vote_simple(election):
num_candidates = election.candidates.count()
- cand_name = {}
- for candidate in election.candidates:
- cand_name[candidate.name] = candidate.id
next_action = 'confirm'
form = forms.get_simple_voting_form(
commit 4102814cecf3649168c511a707b63b3429581f4b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:05:19 2014 +0200
Fixed presenting the fields of the form with their appropriate label/value
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index e4da19f..c58b5ff 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -112,8 +112,8 @@ def get_range_voting_form(candidates, max_range):
for candidate in candidates:
title = candidate.name
- #if candidate.url:
- #title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
+ if candidate.url:
+ title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
field = wtforms.SelectField(
title,
choices=[(str(item), item) for item in range(max_range + 1)]
@@ -130,9 +130,9 @@ def get_simple_voting_form(candidates, max_range):
titles = []
for candidate in candidates:
title = candidate.name
- #if candidate.url:
- #title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
- titles.append((title, title))
+ if candidate.url:
+ title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
+ titles.append((str(candidate.id), title))
field = wtforms.RadioField(
'Candidates',
choices=titles
commit b6ec054f31f93a74f3b086c79a1076923cb36d08
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:04:38 2014 +0200
If there is a usernamemap specified, use it instead of the field label
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
index e0a59b6..24df810 100644
--- a/fedora_elections/templates/_formhelpers.html
+++ b/fedora_elections/templates/_formhelpers.html
@@ -21,9 +21,10 @@
</tr>
{% endmacro %}
-{% macro render_field_data_in_row(field, num_candidates, after="") %}
+{% macro render_field_data_in_row(field, num_candidates, usernamemap=None, after="") %}
<tr>
- <td>{{ field.label }}</td>
+ <td>{% if usernamema %} {{ usernamemap[field.label] }} {%
+ else %} {{ field.label }} {% endif %}</td>
<td>
{% set weightpercent = (field.data | int *100) / num_candidates %}
{% if weightpercent >= 90 %}
commit b7f909ab69a56fb68a99e4beee074edd46aeca9b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:03:57 2014 +0200
No layout change when confirming for the moment
diff --git a/fedora_elections/templates/vote_simple.html b/fedora_elections/templates/vote_simple.html
index 3ebec77..ebc896f 100644
--- a/fedora_elections/templates/vote_simple.html
+++ b/fedora_elections/templates/vote_simple.html
@@ -8,15 +8,10 @@
<p><a href="{{election.url}}">[More Information]</a></p>
-
<form action="{{ url_for('vote', election_alias=election.alias) }}" method="post">
<table border="1" cellpadding="1">
{% for field in form if field.widget.input_type != 'hidden' %}
- {% if nextaction == 'vote' %}
- {{ render_field_data_in_row(field, num_candidates=num_candidates) }}
- {% else %}
{{ render_field_in_row(field) }}
- {% endif %}
{% endfor %}
</table>
commit 5a89394de50f5627833b7f6ddbb7b60e2911f291
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:44:04 2014 +0200
Fix the tests for the change in the simple voting processing and integration of wtforms
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index cbb455b..3478b44 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -152,7 +152,7 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
user = FakeUser(['voters'], username='toshio')
@@ -430,12 +430,12 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
# Invalid vote: No candidate
data = {
- 'name': 'Preview',
+ 'action': 'preview',
}
output = self.app.post('/vote/test_election5', data=data)
@@ -443,16 +443,43 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
self.assertTrue(
- '<li class="message">Please vote for a candidate</li>'
+ '<td class="error">Not a valid choice</td>'
in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
+ in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ # Invalid vote: No candidate
+ data = {
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/vote/test_election5', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<td class="error">Not a valid choice</td>'
+ in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+ self.assertTrue(
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
# Invalid vote: Not numeric
data = {
'candidate': 'a',
- 'name': 'Preview',
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
}
output = self.app.post('/vote/test_election5', data=data)
@@ -460,13 +487,17 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Valid input
data = {
- 'candidate': 7,
- 'name': 'Preview',
+ 'candidate': 'Ralph',
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
}
output = self.app.post('/vote/test_election5', data=data)
@@ -474,7 +505,7 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="confirm" value="Submit" />'
+ '<input type="hidden" name="action" value="submit" />'
in output.data)
self.assertTrue(
'<li class="message">Please confirm your vote!</li>'
@@ -493,51 +524,73 @@ class FlaskElectionstests(ModelFlasktests):
user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
+ # Invalid candidate id - no csrf
+ data = {
+ 'candidate': 1,
+ 'action': 'submit',
+ }
+
+ output = self.app.post(
+ '/vote/test_election5', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
# Invalid candidate id
data = {
'candidate': 1,
- 'name': 'Submit',
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
'/vote/test_election5', data=data,
follow_redirects=True)
self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<li class="error">Invalid vote, this candidate is not '
- 'listed for this election</li>' in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Invalid vote: too low
data = {
'candidate': -1,
- 'name': 'Submit',
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
'/vote/test_election5', data=data,
follow_redirects=True)
self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<li class="error">Invalid vote, this candidate is not '
- 'listed for this election</li>' in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Invalid vote: Not numeric
data = {
'candidate': 'a',
- 'name': 'Submit',
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
'/vote/test_election5', data=data,
follow_redirects=True)
self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<li class="error">Invalid Ballot!</li' in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Valid input
data = {
- 'candidate': 7,
- 'name': 'Submit',
+ 'candidate': 'Toshio',
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
commit ab047e73d58fccdb90f4243ff59434400e7610a9
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:43:33 2014 +0200
Ignore the [more] link for the candidates for the moment, we need a better solution
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index b218468..e4da19f 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -112,8 +112,8 @@ def get_range_voting_form(candidates, max_range):
for candidate in candidates:
title = candidate.name
- if candidate.url:
- title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
+ #if candidate.url:
+ #title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
field = wtforms.SelectField(
title,
choices=[(str(item), item) for item in range(max_range + 1)]
@@ -130,8 +130,8 @@ def get_simple_voting_form(candidates, max_range):
titles = []
for candidate in candidates:
title = candidate.name
- if candidate.url:
- title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
+ #if candidate.url:
+ #title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
titles.append((title, title))
field = wtforms.RadioField(
'Candidates',
commit e33d89fe80dab87f825d0259f582fe3a9ffa97a3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:43:01 2014 +0200
Use candidate instead of candidates to be consistent with the old behavior
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 3e7f7c9..b218468 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -137,6 +137,6 @@ def get_simple_voting_form(candidates, max_range):
'Candidates',
choices=titles
)
- setattr(SimpleVoting, 'candidates', field)
+ setattr(SimpleVoting, 'candidate', field)
return SimpleVoting()
commit b8d48f2e894c0a5f7e8f0e2e01c80fed61080e48
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:30:56 2014 +0200
Adjust the simple voting template to rely on the form just added
diff --git a/fedora_elections/templates/vote_simple.html b/fedora_elections/templates/vote_simple.html
index 77dff56..3ebec77 100644
--- a/fedora_elections/templates/vote_simple.html
+++ b/fedora_elections/templates/vote_simple.html
@@ -1,3 +1,5 @@
+{% from "_formhelpers.html" import render_field_in_row, render_field_data_in_row %}
+
{% extends "base.html" %}
{% block body %}
<h2>{{election.shortdesc}}</h2>
@@ -6,34 +8,30 @@
<p><a href="{{election.url}}">[More Information]</a></p>
+
<form action="{{ url_for('vote', election_alias=election.alias) }}" method="post">
- <table border="0" cellpadding="1">
- {% for candidate in election.candidates|sort %}
- <tr>
- <td>
- {% if candidate.id == candidate_id %}
- <input type="radio" name="candidate" value="{{ candidate.id }}" checked="checked">
- {% else %}
- <input type="radio" name="candidate" value="{{ candidate.id }}">
- {% endif %}
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- <a href="{{candidate.url}}">[Info]</a>
- </td>
- </tr>
+ <table border="1" cellpadding="1">
+ {% for field in form if field.widget.input_type != 'hidden' %}
+ {% if nextaction == 'vote' %}
+ {{ render_field_data_in_row(field, num_candidates=num_candidates) }}
+ {% else %}
+ {{ render_field_in_row(field) }}
+ {% endif %}
{% endfor %}
+
</table>
{% if g.fas_user %}
<p>
- {% if nextaction == 'confirm' %}
+ {% if nextaction == 'vote' %}
+ <input type="hidden" name="action" value="submit" />
<input type="submit" name="{{nextaction}}" value="Submit" />
{% else %}
+ <input type="hidden" name="action" value="preview" />
<input type="submit" name="{{nextaction}}" value="Preview" />
{% endif %}
</p>
+ <a href="{{ url_for('open_elections') }}">Back</a>
+ {{ form.csrf_token }}
{% endif %}
</form>
commit e54eec82a9935437f3ea03218330aef59a42519e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:30:38 2014 +0200
Port the simple voting logic to use the forms just created
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index b1bb8c2..3cf40e3 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -183,51 +183,38 @@ def vote_simple(election):
SESSION, flask.g.fas_user.username, election.id, count=True)
num_candidates = election.candidates.count()
- next_action = 'vote'
- candidate_id = -1
- if flask.request.method == 'POST':
- next_action = ''
- form_values = flask.request.form.values()
- if 'Submit' in form_values:
- try:
- candidates_id = [cand.id for cand in election.candidates]
- candidate_id = int(flask.request.form['candidate'])
- except ValueError:
- flask.flash("Invalid Ballot!", "error")
- return safe_redirect_back()
- if candidate_id not in candidates_id:
- flask.flash("Invalid vote, this candidate is not listed for "
- "this election", "error")
- return safe_redirect_back()
+ cand_name = {}
+ for candidate in election.candidates:
+ cand_name[candidate.name] = candidate.id
+ next_action = 'confirm'
+
+ form = forms.get_simple_voting_form(
+ candidates=election.candidates,
+ max_range=num_candidates)
+
+ if form.validate_on_submit():
+ if form.action.data == 'submit':
+ for candidate in form:
+ if candidate.short_name in ['csrf_token', 'action']:
+ continue
- new_vote = models.Vote(
- election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=candidate_id,
- value=1
- )
- SESSION.add(new_vote)
+ new_vote = models.Vote(
+ election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=cand_name[candidate.data],
+ value=1,
+ )
+ SESSION.add(new_vote)
SESSION.commit()
flask.flash("Your vote has been recorded. Thank you!")
return safe_redirect_back()
- elif 'Preview' in form_values:
- if ('candidate' in flask.request.form):
- try:
- candidate_id = int(flask.request.form['candidate'])
- flask.flash("Please confirm your vote!")
- except ValueError:
- next_action = 'vote'
- flask.flash("Invalid data")
- else:
- flask.flash("Please vote for a candidate")
- next_action = 'vote'
-
- if (next_action != 'vote'):
- next_action = 'confirm'
+ if form.action.data == 'preview':
+ flask.flash("Please confirm your vote!")
+ next_action = 'vote'
usernamemap = {}
if (election.candidates_are_fasusers): # pragma: no cover
@@ -242,9 +229,9 @@ def vote_simple(election):
return flask.render_template(
'vote_simple.html',
election=election,
+ form=form,
num_candidates=num_candidates,
usernamemap=usernamemap,
- candidate_id=candidate_id,
nextaction=next_action)
commit 63b4de6e8caec95b0c22c3319546b3d995e20c9b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:30:15 2014 +0200
Add forms.get_simple_voting_form that dynamically generate the form for simple voting
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 267783e..3e7f7c9 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -121,3 +121,22 @@ def get_range_voting_form(candidates, max_range):
setattr(RangeVoting, candidate.name, field)
return RangeVoting()
+
+
+def get_simple_voting_form(candidates, max_range):
+ class SimpleVoting(wtf.Form):
+ action = wtforms.HiddenField()
+
+ titles = []
+ for candidate in candidates:
+ title = candidate.name
+ if candidate.url:
+ title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
+ titles.append((title, title))
+ field = wtforms.RadioField(
+ 'Candidates',
+ choices=titles
+ )
+ setattr(SimpleVoting, 'candidates', field)
+
+ return SimpleVoting()
commit ce572530f6806492f37e015580112b7d3c530a7b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:19:07 2014 +0200
Rename get_voting_range_form to get_range_voting_form and VotingRange to RangeVoting
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index ade7fc0..b1bb8c2 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -131,7 +131,7 @@ def vote_range(election):
cand_name[candidate.name] = candidate.id
next_action = 'confirm'
- form = forms.get_voting_range_form(
+ form = forms.get_range_voting_form(
candidates=election.candidates,
max_range=num_candidates)
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index bc738af..267783e 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -106,8 +106,8 @@ class ConfirmationForm(wtf.Form):
pass
-def get_voting_range_form(candidates, max_range):
- class VotingRange(wtf.Form):
+def get_range_voting_form(candidates, max_range):
+ class RangeVoting(wtf.Form):
action = wtforms.HiddenField()
for candidate in candidates:
@@ -118,6 +118,6 @@ def get_voting_range_form(candidates, max_range):
title,
choices=[(str(item), item) for item in range(max_range + 1)]
)
- setattr(VotingRange, candidate.name, field)
+ setattr(RangeVoting, candidate.name, field)
- return VotingRange()
+ return RangeVoting()
commit fafd27f9bab0da5e42ae953f20b44f36686cf412
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:09:16 2014 +0200
Fix the tests for the change in range voting
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index 2864ed0..cbb455b 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -144,7 +144,7 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
# Election in progress
@@ -192,7 +192,7 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
# Invalid candidate id
@@ -200,18 +200,40 @@ class FlaskElectionstests(ModelFlasktests):
'9': 1,
'5': 3,
'6': 2,
- 'name': 'Preview',
+ 'action': 'preview',
}
output = self.app.post('/vote/test_election3', data=data)
- self.assertEqual(output.status_code, 400)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 3)
- # Invalid vote: too low
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ # Invalid candidate id
data = {
- '4': -1,
+ '9': 1,
'5': 3,
'6': 2,
- 'name': 'Preview',
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/vote/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 3)
+
+ # Invalid vote: too low
+ data = {
+ 'Toshio': -1,
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
}
output = self.app.post('/vote/test_election3', data=data)
@@ -219,15 +241,19 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Invalid vote: too high
data = {
- '4': 5,
- '5': 3,
- '6': 2,
- 'name': 'Preview',
+ 'Toshio': 5,
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
}
output = self.app.post('/vote/test_election3', data=data)
@@ -235,15 +261,19 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Invalid vote: Not numeric
data = {
- '4': 'a',
- '5': 3,
- '6': 2,
- 'name': 'Preview',
+ 'Toshio': 'a',
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
}
output = self.app.post('/vote/test_election3', data=data)
@@ -251,15 +281,19 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Valid input
data = {
- '4': 1,
- '5': 3,
- '6': 2,
- 'name': 'Preview',
+ 'Toshio': 1,
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
}
output = self.app.post('/vote/test_election3', data=data)
@@ -267,7 +301,7 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="confirm" value="Submit" />'
+ '<input type="hidden" name="action" value="submit" />'
in output.data)
self.assertTrue('<div class="vtmedcool">' in output.data)
self.assertTrue('<div class="vthot">' in output.data)
@@ -286,65 +320,78 @@ class FlaskElectionstests(ModelFlasktests):
user = FakeUser(['voters'], username='pingou')
with user_set(fedora_elections.APP, user):
- # Invalid candidate id
+ # No csrf token provided
data = {
'9': 1,
'5': 3,
'6': 2,
- 'name': 'Submit',
+ 'action': 'submit',
}
output = self.app.post('/vote/test_election3', data=data)
- self.assertEqual(output.status_code, 400)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 3)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
# Invalid vote: too low
data = {
- '4': -1,
- '5': 3,
- '6': 2,
- 'name': 'Submit',
+ 'Toshio': -1,
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
- '/vote/test_election3', data=data, follow_redirects=True)
+ '/vote/test_election3', data=data)
self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<li class="message">Invalid Ballot!</li' in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Invalid vote: too high
data = {
- '4': 5,
- '5': 3,
- '6': 2,
- 'name': 'Submit',
+ 'Toshio': 5,
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
'/vote/test_election3', data=data, follow_redirects=True)
self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<li class="message">Invalid Ballot!</li' in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Invalid vote: Not numeric
data = {
- '4': 'a',
- '5': 3,
- '6': 2,
- 'name': 'Submit',
+ 'Toshio': 'a',
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
'/vote/test_election3', data=data, follow_redirects=True)
self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<li class="message">Invalid Ballot!</li' in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Valid input
data = {
- '4': 1,
- '5': 3,
- '6': 2,
- 'name': 'Submit',
+ 'Toshio': 1,
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
commit 87312b4757c5ef5b64eae9b2edb6bcc55e0310be
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:08:51 2014 +0200
Allow to vote up to the number of candidates
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index d06097c..bc738af 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -116,7 +116,7 @@ def get_voting_range_form(candidates, max_range):
title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
field = wtforms.SelectField(
title,
- choices=[(str(item), item) for item in range(max_range)]
+ choices=[(str(item), item) for item in range(max_range + 1)]
)
setattr(VotingRange, candidate.name, field)
commit 8cee25f11b3bbcdc0d61311b5a2d34b9d1316f3b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 13:29:14 2014 +0200
Adjust the range voting form to make use of the wtforms form created for it
diff --git a/fedora_elections/templates/vote_range.html b/fedora_elections/templates/vote_range.html
index d88c7a8..5e0ff04 100644
--- a/fedora_elections/templates/vote_range.html
+++ b/fedora_elections/templates/vote_range.html
@@ -1,3 +1,5 @@
+{% from "_formhelpers.html" import render_field_in_row, render_field_data_in_row %}
+
{% extends "base.html" %}
{% block body %}
<h2>{{election.shortdesc}}</h2>
@@ -8,73 +10,27 @@
<form action="{{ url_for('vote', election_alias=election.alias) }}" method="post">
<table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- <a href="{{candidate.url}}">[Info]</a>
- </td>
- <!-- If user is logged in: -->
- {% if g.fas_user %}
- {% if nextaction == 'vote' %}
- <td>
- <select name="{{candidate.id}}">
- {% for opt in range(0, num_candidates+1) %}
- <option value={{opt}}>{{opt}}</option>
- {% endfor %}
- </select>
- </td>
- {% endif %}
- {% if nextaction == 'confirm' %}
- <td>
- {% set cid = candidate.id %}
- <input type="hidden" name="{{candidate.id}}"
- value="{{voteinfo[cid]}}" />
- {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
- {% if weightpercent >= 90 %}
- <div class="vthot">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 70 %}
- <div class="vtwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 50 %}
- <div class="vtmedwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 30 %}
- <div class="vtmedcool">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 10 %}
- <div class="vtcool">
- {{voteinfo[cid]}}
- </div>
- {% else %}
- <div class="vtcold">
- {{voteinfo[cid]}}
- </div>
- {% endif %}
- </td>
- {% endif %}
- {% endif %}
- <!-- End if user logged in -->
- </tr>
+ {% for field in form if field.widget.input_type != 'hidden' %}
+ {% if nextaction == 'vote' %}
+ {{ render_field_data_in_row(field, num_candidates=num_candidates) }}
+ {% else %}
+ {{ render_field_in_row(field) }}
+ {% endif %}
{% endfor %}
+
</table>
{% if g.fas_user %}
<p>
- {% if nextaction == 'confirm' %}
+ {% if nextaction == 'vote' %}
+ <input type="hidden" name="action" value="submit" />
<input type="submit" name="{{nextaction}}" value="Submit" />
{% else %}
+ <input type="hidden" name="action" value="preview" />
<input type="submit" name="{{nextaction}}" value="Preview" />
{% endif %}
</p>
+ <a href="{{ url_for('open_elections') }}">Back</a>
+ {{ form.csrf_token }}
{% endif %}
</form>
commit a6c5e2ea52159501d70f9cc7c53150ab378361c2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 13:28:06 2014 +0200
Update the range voting method to relying on a wtforms form
This adds CSRF protection to the voting pages and takes care of the data
validation directly in the form.
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index e477357..ade7fc0 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -126,35 +126,28 @@ def vote_range(election):
num_candidates = election.candidates.count()
- uvotes = {}
+ cand_name = {}
for candidate in election.candidates:
- uvotes[candidate.id] = ""
- next_action = 'vote'
+ cand_name[candidate.name] = candidate.id
+ next_action = 'confirm'
- if flask.request.method == 'POST':
- next_action = ''
- form_values = flask.request.form.values()
- if 'Submit' in form_values:
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote >= 0 and vote <= num_candidates:
- uvotes[candidate.id] = int(vote)
- else:
- flask.flash("Invalid Ballot!")
- return safe_redirect_back()
+ form = forms.get_voting_range_form(
+ candidates=election.candidates,
+ max_range=num_candidates)
- except ValueError:
- flask.flash("Invalid Ballot!")
- return safe_redirect_back()
+ if form.validate_on_submit():
+
+ if form.action.data == 'submit':
+ for candidate in form:
+ if candidate.short_name in ['csrf_token', 'action']:
+ continue
- for uvote in uvotes:
new_vote = models.Vote(
election_id=election.id,
voter=flask.g.fas_user.username,
timestamp=datetime.now(),
- candidate_id=uvote,
- value=uvotes[uvote]
+ candidate_id=cand_name[candidate.short_name],
+ value=candidate.data,
)
SESSION.add(new_vote)
SESSION.commit()
@@ -162,27 +155,9 @@ def vote_range(election):
flask.flash("Your vote has been recorded. Thank you!")
return safe_redirect_back()
- elif 'Preview' in form_values:
+ if form.action.data == 'preview':
flask.flash("Please confirm your vote!")
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote > num_candidates:
- flask.flash("Invalid data.")
- uvotes[candidate.id] = 0
- next_action = 'vote'
- elif vote >= 0:
- uvotes[candidate.id] = vote
- else:
- flask.flash("Invalid data2.")
- uvotes[candidate.id] = 0
- next_action = 'vote'
- except ValueError:
- next_action = 'vote'
- flask.flash("Invalid data")
-
- if next_action != 'vote':
- next_action = 'confirm'
+ next_action = 'vote'
usernamemap = {}
if (election.candidates_are_fasusers): # pragma: no cover
@@ -197,9 +172,9 @@ def vote_range(election):
return flask.render_template(
'vote_range.html',
election=election,
+ form=form,
num_candidates=num_candidates,
usernamemap=usernamemap,
- voteinfo=uvotes,
nextaction=next_action)
commit 9829ab1c16f7e54f5794c706c82aaac08ac2901f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 13:26:57 2014 +0200
Add macro to render the choices when confirming a vote
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
index e11eb67..e0a59b6 100644
--- a/fedora_elections/templates/_formhelpers.html
+++ b/fedora_elections/templates/_formhelpers.html
@@ -20,3 +20,30 @@
%}<td class="error">{{ error }}</td>{% endfor %}{% endif %}
</tr>
{% endmacro %}
+
+{% macro render_field_data_in_row(field, num_candidates, after="") %}
+<tr>
+ <td>{{ field.label }}</td>
+ <td>
+ {% set weightpercent = (field.data | int *100) / num_candidates %}
+ {% if weightpercent >= 90 %}
+ <div class="vthot">
+ {% elif weightpercent >= 70 %}
+ <div class="vtwarm">
+ {% elif weightpercent >= 50 %}
+ <div class="vtmedwarm">
+ {% elif weightpercent >= 30 %}
+ <div class="vtmedcool">
+ {% elif weightpercent >= 10 %}
+ <div class="vtcool">
+ {% else %}
+ <div class="vtcold">
+ {% endif %}
+ {{ field }}
+ </div>
+ </td>
+{% if after %} <td>{{ after }}</td>{% endif %}
+{% if field.errors %}{% for error in field.errors
+%}<td class="error">{{ error }}</td>{% endfor %}{% endif %}
+</tr>
+{% endmacro %}
commit 4e254d05d65ab2256c05869fb4da3b6c5660b7ff
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 13:26:32 2014 +0200
Add dynamically generated form used for range voting
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index f5f40a9..d06097c 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -104,3 +104,20 @@ class MultiCandidateForm(wtf.Form):
class ConfirmationForm(wtf.Form):
pass
+
+
+def get_voting_range_form(candidates, max_range):
+ class VotingRange(wtf.Form):
+ action = wtforms.HiddenField()
+
+ for candidate in candidates:
+ title = candidate.name
+ if candidate.url:
+ title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
+ field = wtforms.SelectField(
+ title,
+ choices=[(str(item), item) for item in range(max_range)]
+ )
+ setattr(VotingRange, candidate.name, field)
+
+ return VotingRange()
commit 74d5baf4ce1584da589bdc058cee9c5d2bcbd0c4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu May 29 18:30:17 2014 +0200
Let's be a little more precise/concrete about what the check allows
diff --git a/fedora_elections/templates/results.html b/fedora_elections/templates/results.html
index ec97fe2..c4581b1 100644
--- a/fedora_elections/templates/results.html
+++ b/fedora_elections/templates/results.html
@@ -21,7 +21,12 @@
{% elif loop.index > election.seats_elected and votes > candidate.vote_count and not flag %}
{# if we are above the number of user that will be selected (seats
available), check if the number of votes for this candidate is lower than
- the number of votes for the last candidate and if the Flag is False #}
+ the number of votes for the last candidate and if the Flag is False
+ So this takes care of the case where there are 10 seats elected and the 11th
+ candidate as the same score as the 10th one.
+ In this case we would end up with one more person that the number of seats
+ available and we'll need to either find a way to select one over the other
+ or deal with having one more candidate accepted #}
{% set flag = True %}
{% set lastrow = True %}
{% else %}
commit 3c38052b6ebb09aefcf53b2e27feda8a72d34af8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu May 29 18:25:20 2014 +0200
Comment the logic of the conditions to select where to place the limit
diff --git a/fedora_elections/templates/results.html b/fedora_elections/templates/results.html
index b8815ff..ec97fe2 100644
--- a/fedora_elections/templates/results.html
+++ b/fedora_elections/templates/results.html
@@ -14,12 +14,20 @@
{% for candidate in election.candidates|sort(attribute='vote_count', reverse=True) %}
{% if loop.index <= election.seats_elected %}
+ {# If we are below the number of user that will be selected, get the number
+ of votes and the flag to False#}
{% set flag = False %}
{% set votes = candidate.vote_count %}
{% elif loop.index > election.seats_elected and votes > candidate.vote_count and not flag %}
+ {# if we are above the number of user that will be selected (seats
+ available), check if the number of votes for this candidate is lower than
+ the number of votes for the last candidate and if the Flag is False #}
{% set flag = True %}
{% set lastrow = True %}
{% else %}
+ {# we are above the number of seats available, the number of votes is below
+ that of the last candidate above selected and the Flag is True which means
+ we already passed the condition above #}
{% set lastrow = False %}
{% endif %}
<tr class="{% if lastrow == True %}firstout{% endif %} {{ loop.cycle('row_odd', 'row_even') }}">
commit fd8286e02e53c94898c174533e623a5b12a81052
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 19:00:25 2014 +0200
Show the actual results in the result page
Rework the result table to display a nice red-bar where the limit between
the elected candidates and the non-elected candidates is.
diff --git a/fedora_elections/static/elections.css b/fedora_elections/static/elections.css
index 8aba889..5f67a8a 100644
--- a/fedora_elections/static/elections.css
+++ b/fedora_elections/static/elections.css
@@ -49,6 +49,15 @@ body#admin table {
padding-top:2em;
}
+#results .firstout tr,
+#results .firstout td {
+ border-top: 2px solid red !important;
+}
+
+.row_even, .row_even td {
+ background-color: #F0F0F0;
+}
+
#content {
float:left;
width:74%px;
diff --git a/fedora_elections/templates/results.html b/fedora_elections/templates/results.html
index cc5e849..b8815ff 100644
--- a/fedora_elections/templates/results.html
+++ b/fedora_elections/templates/results.html
@@ -5,13 +5,24 @@
{% if election.url %}
<p><a href="{{ election.url }}">[More Information]</a></p>
{% endif %}
- <table border="1" cellpadding="1">
+ <table id="results">
<tr>
<th>Candidate</th>
<th title="Number of votes received">Votes</th>
</tr>
- {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
- <tr>
+
+
+ {% for candidate in election.candidates|sort(attribute='vote_count', reverse=True) %}
+ {% if loop.index <= election.seats_elected %}
+ {% set flag = False %}
+ {% set votes = candidate.vote_count %}
+ {% elif loop.index > election.seats_elected and votes > candidate.vote_count and not flag %}
+ {% set flag = True %}
+ {% set lastrow = True %}
+ {% else %}
+ {% set lastrow = False %}
+ {% endif %}
+ <tr class="{% if lastrow == True %}firstout{% endif %} {{ loop.cycle('row_odd', 'row_even') }}">
<td>
{% if election.candidates_are_fasusers %}
{{ usernamemap[candidate.id] }}
@@ -31,6 +42,10 @@
<h3>Some statistics about this election</h3>
<table>
<tr>
+ <th>Number of seats elections</th>
+ <td>{{ election.seats_elected }}</td>
+ </tr>
+ <tr>
<th>Number of voters</th>
<td>{{ stats['n_voters'] }}</td>
</tr>
commit e496781f7b9067b9166588f20ea6dd2f8adef7d8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 18:58:32 2014 +0200
Avoid adding empty legal voter groups
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index e70be01..32a4ee6 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -98,11 +98,12 @@ def admin_new_election():
# Add legal voters groups if there are any
for voter_grp in form.lgl_voters.data.split(','):
- lglvoters = models.LegalVoter(
- election=election,
- group_name=voter_grp
- )
- SESSION.add(lglvoters)
+ if voter_grp.strip():
+ lglvoters = models.LegalVoter(
+ election=election,
+ group_name=voter_grp
+ )
+ SESSION.add(lglvoters)
SESSION.commit()
commit 2280f067602b36f25820e691e2c63f568e01e171
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:57:49 2014 +0200
Cover adding admin groups when testing admin_new_election
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index 838b92e..5f0402a 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -269,6 +269,7 @@ class FlaskAdmintests(ModelFlasktests):
'seats_elected': 2,
'candidates_are_fasusers': False,
'embargoed': True,
+ 'admin_grp': 'testers, sysadmin-main,,',
'csrf_token': csrf_token,
}
@@ -282,6 +283,9 @@ class FlaskAdmintests(ModelFlasktests):
'<a href="/admin/new_election/edit">' in output.data)
self.assertTrue(
'<p>There are no candidates.</p>' in output.data)
+ self.assertTrue(
+ '<li>Admin groups: sysadmin-main, testers</li>'
+ in output.data)
def test_admin_edit_election(self):
""" Test the admin_edit_election function. """
commit e521ec9eab264a92dbcdc099d4fcdc809cd12b14
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:55:37 2014 +0200
Split the tests for editing admin groups or legal voters into their own method
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index 6a9b93e..838b92e 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -385,6 +385,23 @@ class FlaskAdmintests(ModelFlasktests):
'<p>3 candidates found</p>'
in output.data)
+ def test_admin_edit_election_admin_groups(self):
+ """ Test the admin_edit_election function when editing admin groups. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/edit')
+ self.assertEqual(output.status_code, 404)
+
+ self.setup_db()
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election2/edit')
+ self.assertEqual(output.status_code, 200)
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
# Edit Admin Group
# Check election before edit
@@ -452,6 +469,22 @@ class FlaskAdmintests(ModelFlasktests):
'<li>Admin groups: sysadmin-main</li>'
in output.data)
+ def test_admin_edit_election_legal_voters(self):
+ """ Test the admin_edit_election function when editing legal voters. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/edit')
+ self.assertEqual(output.status_code, 404)
+
+ self.setup_db()
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election3/edit')
+ self.assertEqual(output.status_code, 200)
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
# Edit LegalVoter Group
# Check election before edit
commit 6885bc78264e502c312016016d7fd3e84d544791
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:50:56 2014 +0200
Add test checking the behavior when the user is not part of the LegalVoters
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index 74b5126..2864ed0 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -99,6 +99,19 @@ class FlaskElectionstests(ModelFlasktests):
'<li class="error">You need to be in one another group than '
'CLA to vote</li>' in output.data)
+ user = FakeUser(['packager'], username='pingou')
+ with user_set(fedora_elections.APP, user):
+
+ output = self.app.get('/vote/test_election3')
+ self.assertEqual(output.status_code, 302)
+
+ # Election closed and results open
+ output = self.app.get(
+ '/vote/test_election3', follow_redirects=True)
+ self.assertTrue(
+ '<li class="error">You are not among the groups that are '
+ 'allowed to vote for this election</li>' in output.data)
+
user = FakeUser(['voters'], username='pingou')
with user_set(fedora_elections.APP, user):
commit 93afafdceaa67a2fca0b69e5d30c96bc3b531a2c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:48:28 2014 +0200
Add test for editing an election with legal voters
These tests are checking how the application behaves when we add or remove
legal voter groups
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index fb408eb..6a9b93e 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -416,9 +416,9 @@ class FlaskAdmintests(ModelFlasktests):
self.assertTrue(
'<li class="message">Election "test_election2" saved</li>'
in output.data)
- # We edited the seats_elected from 1 to 2
self.assertTrue(
'<li>Number elected: 2</li>' in output.data)
+ # We edited the admin groups
self.assertTrue(
'<li>Admin groups: sysadmin-main, testers</li>'
in output.data)
@@ -445,13 +445,77 @@ class FlaskAdmintests(ModelFlasktests):
self.assertTrue(
'<li class="message">Election "test_election2" saved</li>'
in output.data)
- # We edited the seats_elected from 1 to 2
self.assertTrue(
'<li>Number elected: 2</li>' in output.data)
+ # We edited the admin groups
self.assertTrue(
'<li>Admin groups: sysadmin-main</li>'
in output.data)
+ # Edit LegalVoter Group
+
+ # Check election before edit
+ output = self.app.get('/admin/test_election3/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<li>Number elected: 1</li>' in output.data)
+ self.assertTrue('<li>Admin groups: </li>' in output.data)
+ self.assertTrue('<li>Legal voters: voters</li>' in output.data)
+
+ # Add a new admin group: sysadmin-main
+ data = {
+ 'alias': 'test_election3',
+ 'shortdesc': 'test election 3 shortdesc',
+ 'description': 'test election 3 description',
+ 'voting_type': 'range',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY - timedelta(days=2),
+ 'end_date': TODAY + timedelta(days=3),
+ 'seats_elected': '1',
+ 'candidates_are_fasusers': False,
+ 'embargoed': False,
+ 'lgl_voters': 'voters, sysadmin-main',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election3/edit', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Election "test_election3" saved</li>'
+ in output.data)
+ # We edited the legal_voters
+ self.assertTrue(
+ '<li>Legal voters: sysadmin-main, voters</li>'
+ in output.data)
+
+ # Remove an existing group: voters
+ data = {
+ 'alias': 'test_election3',
+ 'shortdesc': 'test election 3 shortdesc',
+ 'description': 'test election 3 description',
+ 'voting_type': 'range',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY - timedelta(days=2),
+ 'end_date': TODAY + timedelta(days=3),
+ 'seats_elected': '1',
+ 'candidates_are_fasusers': False,
+ 'embargoed': False,
+ 'lgl_voters': 'sysadmin-main',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election3/edit', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Election "test_election3" saved</li>'
+ in output.data)
+ # We edited the legal_voters
+ self.assertTrue(
+ '<li>Legal voters: sysadmin-main</li>'
+ in output.data)
+
def test_admin_add_candidate(self):
""" Test the admin_add_candidate function. """
user = FakeUser(
commit 701a20cf1c1ab14a4db8b1202736a52cc5f70dc8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:48:04 2014 +0200
Adjust the tests so that the FakeFasUser is part of the group allowed to vote
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index 3cd26e0..74b5126 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -99,7 +99,7 @@ class FlaskElectionstests(ModelFlasktests):
'<li class="error">You need to be in one another group than '
'CLA to vote</li>' in output.data)
- user = FakeUser(['packager'], username='pingou')
+ user = FakeUser(['voters'], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get('/vote/test_election')
@@ -142,7 +142,7 @@ class FlaskElectionstests(ModelFlasktests):
'<input type="submit" name="vote" value="Preview" />'
in output.data)
- user = FakeUser(['packager'], username='toshio')
+ user = FakeUser(['voters'], username='toshio')
with user_set(fedora_elections.APP, user):
# Election in progress
@@ -164,7 +164,7 @@ class FlaskElectionstests(ModelFlasktests):
self.setup_db()
- user = FakeUser(['packager'], username='toshio')
+ user = FakeUser(['voters'], username='toshio')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/vote/test_election3', follow_redirects=True)
@@ -172,7 +172,7 @@ class FlaskElectionstests(ModelFlasktests):
'class="message">You have already voted in the election!</'
in output.data)
- user = FakeUser(['packager'], username='pingou')
+ user = FakeUser(['voters'], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/vote/test_election3')
@@ -271,7 +271,7 @@ class FlaskElectionstests(ModelFlasktests):
self.setup_db()
- user = FakeUser(['packager'], username='pingou')
+ user = FakeUser(['voters'], username='pingou')
with user_set(fedora_elections.APP, user):
# Invalid candidate id
data = {
commit 4064980cf19599c1571d226917e5c6b01bee7370
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:47:48 2014 +0200
Specify a group of user allowed to vote on election #3
diff --git a/tests/test_election.py b/tests/test_election.py
index 4a8ed5b..342c253 100644
--- a/tests/test_election.py
+++ b/tests/test_election.py
@@ -108,6 +108,15 @@ class Electiontests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ # Add a LegalVoter group to election #3
+ voters = models.LegalVoter(
+ election=obj,
+ group_name='voters',
+ )
+ self.session.add(voters)
+ self.session.commit()
+ self.assertNotEqual(obj, admin)
+
# Election created but not open
obj = models.Election( # id:4
shortdesc='test election 4 shortdesc',
commit 61e6b277e7e95aa385e43cf6fcb74bc17771be29
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:37:20 2014 +0200
Add test checking the error message if the user has 0 groups
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index bdd73eb..3cd26e0 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -87,6 +87,18 @@ class FlaskElectionstests(ModelFlasktests):
'<li class="error">You must sign the CLA to vote</li>'
in output.data)
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+
+ output = self.app.get('/vote/test_election')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get(
+ '/vote/test_election', follow_redirects=True)
+ self.assertTrue(
+ '<li class="error">You need to be in one another group than '
+ 'CLA to vote</li>' in output.data)
+
user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
commit 10d59f2da65aac5cede2e83f8a5303d0bda7676e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:36:39 2014 +0200
Fix the unit-tests so that the FakeFasUser used has at least 1 group
And fix checking the error message with cla_done = False
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index e808963..bdd73eb 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -55,7 +55,7 @@ class FlaskElectionstests(ModelFlasktests):
'<title>OpenID transaction in progress</title>' in output.data
or 'discoveryfailure' in output.data)
- user = FakeUser([], username='pingou')
+ user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get('/vote/test_election')
self.assertEqual(output.status_code, 302)
@@ -84,10 +84,10 @@ class FlaskElectionstests(ModelFlasktests):
output = self.app.get(
'/vote/test_election', follow_redirects=True)
self.assertTrue(
- '<li class="error">You need to be in one another group than '
- 'CLA to vote</li>' in output.data)
+ '<li class="error">You must sign the CLA to vote</li>'
+ in output.data)
- user = FakeUser([], username='pingou')
+ user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get('/vote/test_election')
@@ -130,7 +130,7 @@ class FlaskElectionstests(ModelFlasktests):
'<input type="submit" name="vote" value="Preview" />'
in output.data)
- user = FakeUser([], username='toshio')
+ user = FakeUser(['packager'], username='toshio')
with user_set(fedora_elections.APP, user):
# Election in progress
@@ -152,7 +152,7 @@ class FlaskElectionstests(ModelFlasktests):
self.setup_db()
- user = FakeUser([], username='toshio')
+ user = FakeUser(['packager'], username='toshio')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/vote/test_election3', follow_redirects=True)
@@ -160,7 +160,7 @@ class FlaskElectionstests(ModelFlasktests):
'class="message">You have already voted in the election!</'
in output.data)
- user = FakeUser([], username='pingou')
+ user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/vote/test_election3')
@@ -259,7 +259,7 @@ class FlaskElectionstests(ModelFlasktests):
self.setup_db()
- user = FakeUser([], username='pingou')
+ user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
# Invalid candidate id
data = {
@@ -343,7 +343,7 @@ class FlaskElectionstests(ModelFlasktests):
self.setup_db()
- user = FakeUser([], username='toshio')
+ user = FakeUser(['packager'], username='toshio')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/vote/test_election5', follow_redirects=True)
@@ -351,7 +351,7 @@ class FlaskElectionstests(ModelFlasktests):
'class="message">You have already voted in the election!</'
in output.data)
- user = FakeUser([], username='pingou')
+ user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/vote/test_election5')
@@ -419,7 +419,7 @@ class FlaskElectionstests(ModelFlasktests):
self.setup_db()
- user = FakeUser([], username='pingou')
+ user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
# Invalid candidate id
data = {
@@ -508,7 +508,7 @@ class FlaskElectionstests(ModelFlasktests):
'pending formal announcement.</li>' in output.data)
self.assertTrue('<h3>Current elections</h3>' in output.data)
- user = FakeUser([], username='toshio')
+ user = FakeUser(['packager'], username='toshio')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/results/test_election2', follow_redirects=True)
@@ -541,9 +541,7 @@ class FlaskElectionstests(ModelFlasktests):
'<h3>Some statistics about this election</h3>'
in output.data)
- user = FakeUser(
- ['gitr2spec'],
- username='kevin')
+ user = FakeUser(['gitr2spec'], username='kevin')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/results/test_election3', follow_redirects=True)
commit cb9204232af452a1e4ee315862f93e3cc7429eaf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:34:49 2014 +0200
Fix the @login_required to check to cla_done and groups > 0
This commit fixes @login_required that was checking cla_done and returning
an error message about CLA+1.
We now check cla_done and groups > 0 and report an appropriate error message
in both cases.
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index b8b7885..e477357 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -46,6 +46,10 @@ def login_required(f):
'auth_login', next=flask.request.url))
elif not flask.g.fas_user.cla_done:
flask.flash(
+ 'You must sign the CLA to vote', 'error')
+ return safe_redirect_back()
+ elif len(flask.g.fas_user.groups) == 0:
+ flask.flash(
'You need to be in one another group than CLA to vote',
'error')
return safe_redirect_back()
commit 598338d52a50a52052d3a68a3d735cfcd6a0ec1d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:30:52 2014 +0200
Add test for the redirect from the voting page is the user did not sign the CLA
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index 6506a08..e808963 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -75,6 +75,18 @@ class FlaskElectionstests(ModelFlasktests):
self.setup_db()
+ user = FakeUser([], username='pingou', cla_done=False)
+ with user_set(fedora_elections.APP, user):
+
+ output = self.app.get('/vote/test_election')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get(
+ '/vote/test_election', follow_redirects=True)
+ self.assertTrue(
+ '<li class="error">You need to be in one another group than '
+ 'CLA to vote</li>' in output.data)
+
user = FakeUser([], username='pingou')
with user_set(fedora_elections.APP, user):
commit 3c721970d437a1f5501e27a3d6d56cb0ae71bdde
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:12:20 2014 +0200
If there are legal voters and the user is not one of them, redirect him/her back
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index d82bae7..b8b7885 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -91,6 +91,14 @@ def vote(election_alias):
if not isinstance(election, models.Election):
return election
+ if election.legal_voters_list:
+ if len(set(flask.g.fas_user.groups).intersection(
+ set(election.legal_voters_list))) == 0:
+ flask.flash(
+ 'You are not among the groups that are allowed to vote for this '
+ 'election', 'error')
+ return safe_redirect_back()
+
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
commit f51906fa56568e9d749c25e96a82646546c8491b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:10:59 2014 +0200
Use safe_redirect_back instead of forcing the return to index
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 92f64de..d82bae7 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -48,7 +48,7 @@ def login_required(f):
flask.flash(
'You need to be in one another group than CLA to vote',
'error')
- return flask.redirect(flask.url_for('index'))
+ return safe_redirect_back()
return f(*args, **kwargs)
return decorated_function
commit c92438f6ca22301152dfa327c6d2f04479938de7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:08:03 2014 +0200
Voting to an election requires CLA+1 at minimum
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index f14a900..92f64de 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -44,6 +44,12 @@ def login_required(f):
if not is_authenticated():
return flask.redirect(flask.url_for(
'auth_login', next=flask.request.url))
+ elif not flask.g.fas_user.cla_done:
+ flask.flash(
+ 'You need to be in one another group than CLA to vote',
+ 'error')
+ return flask.redirect(flask.url_for('index'))
+
return f(*args, **kwargs)
return decorated_function
commit 6e57cd622cba3ea37d8fac846a452a91c7cc502e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:57:02 2014 +0200
Implement legal_voters adding/editing/deleting
This allow the admin to restrict the election to one or more FAS groups
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index 6d568c9..e70be01 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -87,12 +87,22 @@ def admin_new_election():
time(23, 59, 59))
SESSION.add(election)
+ # Add admin groups if there are any
for admin_grp in form.admin_grp.data.split(','):
- admin = models.ElectionAdminGroup(
+ if admin_grp.strip():
+ admin = models.ElectionAdminGroup(
+ election=election,
+ group_name=admin_grp.strip(),
+ )
+ SESSION.add(admin)
+
+ # Add legal voters groups if there are any
+ for voter_grp in form.lgl_voters.data.split(','):
+ lglvoters = models.LegalVoter(
election=election,
- group_name=admin_grp.strip(),
+ group_name=voter_grp
)
- SESSION.add(admin)
+ SESSION.add(lglvoters)
SESSION.commit()
@@ -145,7 +155,7 @@ def admin_edit_election(election_alias):
new_groups = set(
[grp.strip() for grp in form.admin_grp.data.split(',')])
- # Add the new groups
+ # Add the new admin groups
for admin_grp in new_groups.difference(admin_groups):
admin = models.ElectionAdminGroup(
election=election,
@@ -153,12 +163,32 @@ def admin_edit_election(election_alias):
)
SESSION.add(admin)
- # Remove the groups that were removed with this edition
+ # Remove the admin groups that were removed with this edition
for admin_grp in admin_groups.difference(new_groups):
admingrp = models.ElectionAdminGroup.by_election_id_and_name(
SESSION, election.id, admin_grp)
SESSION.delete(admingrp)
+ legal_voters = set(election.legal_voters_list)
+
+ new_lgl_voters_groups = set(
+ [grp.strip() for grp in form.lgl_voters.data.split(',')
+ if grp.strip()])
+
+ # Add the new legal voter groups
+ for lgl_grp in new_lgl_voters_groups.difference(legal_voters):
+ admin = models.LegalVoter(
+ election=election,
+ group_name=lgl_grp,
+ )
+ SESSION.add(admin)
+
+ # Remove the legal voter groups that were removed with this edition
+ for lgl_grp in legal_voters.difference(new_lgl_voters_groups):
+ admingrp = models.LegalVoter.by_election_id_and_name(
+ SESSION, election.id, lgl_grp)
+ SESSION.delete(admingrp)
+
SESSION.commit()
fedmsgshim.publish(
topic="election.edit",
@@ -172,6 +202,7 @@ def admin_edit_election(election_alias):
'admin_view_election', election_alias=election.alias))
form.admin_grp.data = ', '.join(election.admin_groups_list)
+ form.lgl_voters.data = ', '.join(election.legal_voters_list)
return flask.render_template(
'admin/election_form.html',
form=form,
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index c066f2d..f5f40a9 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -59,6 +59,9 @@ class ElectionForm(wtf.Form):
embargoed = wtforms.BooleanField('Embargo results?', default=True)
+ lgl_voters = wtforms.TextField(
+ 'Legal voters groups', [wtforms.validators.optional()])
+
admin_grp = wtforms.TextField(
'Admin groups', [wtforms.validators.optional()])
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index ff637e8..e125545 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -116,6 +116,10 @@ class Election(BASE):
return sorted([grp.group_name for grp in self.admin_groups])
@property
+ def legal_voters_list(self):
+ return sorted([grp.group_name for grp in self.legal_voters])
+
+ @property
def status(self):
now = datetime.utcnow()
if now.date() < self.start_date.date():
@@ -298,6 +302,19 @@ class LegalVoter(BASE):
# special names:
# "cla + one" = cla_done + 1 non-cla group
+ @classmethod
+ def by_election_id_and_name(cls, session, election_id, group_name):
+ """ Return the ElectionAdminGroup having a specific election_id
+ and group_name.
+ """
+ return session.query(
+ cls
+ ).filter(
+ cls.election_id == election_id
+ ).filter(
+ cls.group_name == group_name
+ ).first()
+
class Vote(BASE):
__tablename__ = 'votes'
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index bd0fa6d..050c94d 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -21,7 +21,8 @@
{{ render_field_in_row(form.seats_elected) }}
{{ render_field_in_row(form.candidates_are_fasusers) }}
{{ render_field_in_row(form.embargoed) }}
- {{ render_field_in_row(form.admin_grp, after="FAS group allowed to view the result despite the embargo") }}
+ {{ render_field_in_row(form.lgl_voters, after="FAS groups allowed to vote on this election") }}
+ {{ render_field_in_row(form.admin_grp, after="FAS groups allowed to view the result despite the embargo") }}
</table>
</fieldset>
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index 8726599..90ab10b 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -16,6 +16,7 @@
<li>Number elected: {{ election.seats_elected }}</li>
<li>Election type: {{ election.voting_type }}</li>
<li>Admin groups: {{ election.admin_groups_list | join(', ') }}</li>
+ <li>Legal voters: {{ election.legal_voters_list | join(', ') }}</li>
</ul>
<h3>Candidates</h3>
{% if election.candidates.all() %}
commit 4c5033252196111730187cd0496c4f80b7899c71
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:25:45 2014 +0200
Simplify vote_range and vote_simple since they are no longer flask endpoints
- We can directly give them the proper election object, no need to query
it twice
- We can skips the tests we do earlier (avoids doing them twice)
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 23d94ee..f14a900 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -93,31 +93,19 @@ def vote(election_alias):
return safe_redirect_back()
if election.voting_type == 'range':
- return vote_range(election_alias)
+ return vote_range(election)
elif election.voting_type == 'simple':
- return vote_simple(election_alias)
+ return vote_simple(election)
else: # pragma: no cover
flask.flash(
'Unknown election voting type: %s' % election.voting_type)
return safe_redirect_back()
-def vote_range(election_alias):
- election = get_valid_election(election_alias)
-
- if not isinstance(election, models.Election): # pragma: no cover
- return election
-
- if (election.voting_type == 'simple'):
- return flask.redirect(flask.url_for(
- 'vote_simple', election_alias=election_alias))
+def vote_range(election):
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
- if votes > 0:
- flask.flash('You have already voted in the election!')
- return safe_redirect_back()
-
num_candidates = election.candidates.count()
uvotes = {}
@@ -197,23 +185,10 @@ def vote_range(election_alias):
nextaction=next_action)
-def vote_simple(election_alias):
- election = get_valid_election(election_alias)
-
- if not isinstance(election, models.Election): # pragma: no cover
- return election
-
- if (election.voting_type == 'range'):
- return flask.redirect(flask.url_for(
- 'vote_range', election_alias=election_alias))
-
+def vote_simple(election):
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
- if (votes != 0):
- flask.flash('You have already voted in the election!')
- return safe_redirect_back()
-
num_candidates = election.candidates.count()
next_action = 'vote'
candidate_id = -1
commit 5781e3534e267f6d23d8e81ea587d4e96354905a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:22:04 2014 +0200
Adjust unit-tests to the unique URL for voting
- Replace vote_range and vote_simple by vote
- Do not test for redirect as it will always return the proper view
diff --git a/tests/test_flask.py b/tests/test_flask.py
index d18e633..1ff7efe 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -80,7 +80,7 @@ class Flasktests(ModelFlasktests):
self.assertTrue('<h3>Current elections</h3>' in output.data)
self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
- self.assertTrue('<a href="/vote_range/' in output.data)
+ self.assertTrue('<a href="/vote/' in output.data)
self.assertTrue(
'<span class="text">logged in as </span>' in output.data)
self.assertTrue('Vote now!' in output.data)
@@ -282,7 +282,7 @@ class Flasktests(ModelFlasktests):
self.assertTrue('<title>Fedora elections</title>' in output.data)
self.assertTrue('<h3>Next 2 elections</h3>' in output.data)
self.assertTrue('<td>test election 3 shortdesc</td>' in output.data)
- self.assertTrue('<a href="/vote_range/' in output.data)
+ self.assertTrue('<a href="/vote/' in output.data)
self.assertTrue('<a href="/login">login</a>' in output.data)
user = FakeUser([], username='pingou')
@@ -291,7 +291,7 @@ class Flasktests(ModelFlasktests):
self.assertEqual(output.status_code, 200)
self.assertTrue('<title>Fedora elections</title>' in output.data)
self.assertTrue('<h3>Next 2 elections</h3>' in output.data)
- self.assertTrue('<a href="/vote_range/' in output.data)
+ self.assertTrue('<a href="/vote/' in output.data)
self.assertTrue(
'<span class="text">logged in as </span>' in output.data)
self.assertTrue('Vote now!' in output.data)
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index ae72b46..6506a08 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -132,7 +132,7 @@ class FlaskElectionstests(ModelFlasktests):
def test_vote_range(self):
""" Test the vote_range function - the preview part. """
output = self.app.get(
- '/vote_range/test_election', follow_redirects=True)
+ '/vote/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<title>OpenID transaction in progress</title>' in output.data
@@ -143,7 +143,7 @@ class FlaskElectionstests(ModelFlasktests):
user = FakeUser([], username='toshio')
with user_set(fedora_elections.APP, user):
output = self.app.get(
- '/vote_range/test_election3', follow_redirects=True)
+ '/vote/test_election3', follow_redirects=True)
self.assertTrue(
'class="message">You have already voted in the election!</'
in output.data)
@@ -151,13 +151,7 @@ class FlaskElectionstests(ModelFlasktests):
user = FakeUser([], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get(
- '/vote_range/test_election5')
- self.assertTrue(
- 'be redirected automatically to target URL: '
- '<a href="/vote_simple/test_election5">' in output.data)
-
- output = self.app.get(
- '/vote_range/test_election3')
+ '/vote/test_election3')
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
self.assertTrue(
@@ -172,7 +166,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_range/test_election3', data=data)
+ output = self.app.post('/vote/test_election3', data=data)
self.assertEqual(output.status_code, 400)
# Invalid vote: too low
@@ -183,7 +177,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_range/test_election3', data=data)
+ output = self.app.post('/vote/test_election3', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
@@ -199,7 +193,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_range/test_election3', data=data)
+ output = self.app.post('/vote/test_election3', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
@@ -215,7 +209,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_range/test_election3', data=data)
+ output = self.app.post('/vote/test_election3', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
@@ -231,7 +225,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_range/test_election3', data=data)
+ output = self.app.post('/vote/test_election3', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
@@ -245,7 +239,7 @@ class FlaskElectionstests(ModelFlasktests):
def test_vote_range_process(self):
""" Test the vote_range function - the voting part. """
output = self.app.get(
- '/vote_range/test_election', follow_redirects=True)
+ '/vote/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<title>OpenID transaction in progress</title>' in output.data
@@ -263,7 +257,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Submit',
}
- output = self.app.post('/vote_range/test_election3', data=data)
+ output = self.app.post('/vote/test_election3', data=data)
self.assertEqual(output.status_code, 400)
# Invalid vote: too low
@@ -275,7 +269,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_range/test_election3', data=data, follow_redirects=True)
+ '/vote/test_election3', data=data, follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<li class="message">Invalid Ballot!</li' in output.data)
@@ -289,7 +283,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_range/test_election3', data=data, follow_redirects=True)
+ '/vote/test_election3', data=data, follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<li class="message">Invalid Ballot!</li' in output.data)
@@ -303,7 +297,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_range/test_election3', data=data, follow_redirects=True)
+ '/vote/test_election3', data=data, follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<li class="message">Invalid Ballot!</li' in output.data)
@@ -317,7 +311,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_range/test_election3', data=data, follow_redirects=True)
+ '/vote/test_election3', data=data, follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'class="message">Your vote has been recorded. Thank you!</'
@@ -329,7 +323,7 @@ class FlaskElectionstests(ModelFlasktests):
def test_vote_simple(self):
""" Test the vote_simple function - the preview part. """
output = self.app.get(
- '/vote_simple/test_election', follow_redirects=True)
+ '/vote/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<title>OpenID transaction in progress</title>' in output.data
@@ -340,20 +334,15 @@ class FlaskElectionstests(ModelFlasktests):
user = FakeUser([], username='toshio')
with user_set(fedora_elections.APP, user):
output = self.app.get(
- '/vote_simple/test_election5', follow_redirects=True)
+ '/vote/test_election5', follow_redirects=True)
self.assertTrue(
'class="message">You have already voted in the election!</'
in output.data)
user = FakeUser([], username='pingou')
with user_set(fedora_elections.APP, user):
- output = self.app.get('/vote_simple/test_election3')
- self.assertTrue(
- 'be redirected automatically to target URL: '
- '<a href="/vote_range/test_election3">' in output.data)
-
output = self.app.get(
- '/vote_simple/test_election5')
+ '/vote/test_election5')
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
self.assertTrue(
@@ -365,7 +354,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_simple/test_election5', data=data)
+ output = self.app.post('/vote/test_election5', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
@@ -382,7 +371,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_simple/test_election5', data=data)
+ output = self.app.post('/vote/test_election5', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
@@ -396,7 +385,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_simple/test_election5', data=data)
+ output = self.app.post('/vote/test_election5', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
@@ -410,7 +399,7 @@ class FlaskElectionstests(ModelFlasktests):
def test_vote_simple_process(self):
""" Test the vote_simple function - the voting part. """
output = self.app.get(
- '/vote_simple/test_election', follow_redirects=True)
+ '/vote/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<title>OpenID transaction in progress</title>' in output.data
@@ -427,7 +416,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data,
+ '/vote/test_election5', data=data,
follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
@@ -441,7 +430,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data,
+ '/vote/test_election5', data=data,
follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
@@ -455,7 +444,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data,
+ '/vote/test_election5', data=data,
follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
@@ -468,7 +457,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data,
+ '/vote/test_election5', data=data,
follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
commit f01af3bfe463072d2448c9e0a708e8a1dd063388
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:21:40 2014 +0200
Adjust the action url in the vote templates now that there is a unique URL for voting
diff --git a/fedora_elections/templates/vote_range.html b/fedora_elections/templates/vote_range.html
index 71a7ee1..d88c7a8 100644
--- a/fedora_elections/templates/vote_range.html
+++ b/fedora_elections/templates/vote_range.html
@@ -6,7 +6,7 @@
<p><a href="{{election.url}}">[More Information]</a></p>
-<form action="" method="post">
+<form action="{{ url_for('vote', election_alias=election.alias) }}" method="post">
<table border="1" cellpadding="1">
{% for candidate in election.candidates %}
<tr>
diff --git a/fedora_elections/templates/vote_simple.html b/fedora_elections/templates/vote_simple.html
index 0d43183..77dff56 100644
--- a/fedora_elections/templates/vote_simple.html
+++ b/fedora_elections/templates/vote_simple.html
@@ -6,7 +6,7 @@
<p><a href="{{election.url}}">[More Information]</a></p>
-<form action="" method="post">
+<form action="{{ url_for('vote', election_alias=election.alias) }}" method="post">
<table border="0" cellpadding="1">
{% for candidate in election.candidates|sort %}
<tr>
commit 1809a2b157c67f5302a10f944340da31845a314d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:03:07 2014 +0200
No need to have different URL for the different voting type, one is enough
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 8f1b1a3..23d94ee 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -101,9 +101,6 @@ def vote(election_alias):
'Unknown election voting type: %s' % election.voting_type)
return safe_redirect_back()
-
-(a)APP.route('/vote_range/<election_alias>', methods=['GET', 'POST'])
-@login_required
def vote_range(election_alias):
election = get_valid_election(election_alias)
@@ -200,8 +197,6 @@ def vote_range(election_alias):
nextaction=next_action)
-(a)APP.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
-@login_required
def vote_simple(election_alias):
election = get_valid_election(election_alias)
diff --git a/fedora_elections/templates/index.html b/fedora_elections/templates/index.html
index 8f70756..45518d4 100644
--- a/fedora_elections/templates/index.html
+++ b/fedora_elections/templates/index.html
@@ -23,11 +23,7 @@
<td>
{% if election.status == "In progress" %}
{% if election not in voted %}
- {% if election.voting_type == 'range' %}
- <a href="{{ url_for('vote_range', election_alias=election.alias) }}">
- {% elif election.voting_type == 'simple' %}
- <a href="{{ url_for('vote_simple', election_alias=election.alias) }}">
- {% endif %}
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">
Vote now!
</a>
{% endif %}
commit 3d7e19d7cdc085f128ca02ecc14d6744515c9b59
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:36:36 2014 +0200
Explain a little what the admin groups are
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index dd42f1c..bd0fa6d 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -21,7 +21,7 @@
{{ render_field_in_row(form.seats_elected) }}
{{ render_field_in_row(form.candidates_are_fasusers) }}
{{ render_field_in_row(form.embargoed) }}
- {{ render_field_in_row(form.admin_grp) }}
+ {{ render_field_in_row(form.admin_grp, after="FAS group allowed to view the result despite the embargo") }}
</table>
</fieldset>
commit 915284ab8d9a2adf202be45c917e416282dcc7c8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:29:35 2014 +0200
pep8 fixes
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index c766962..ff637e8 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -245,8 +245,6 @@ class ElectionAdminGroup(BASE):
).first()
-
-
class Candidate(BASE):
__tablename__ = 'candidates'
diff --git a/tests/test_election.py b/tests/test_election.py
index 313a98e..4a8ed5b 100644
--- a/tests/test_election.py
+++ b/tests/test_election.py
@@ -83,9 +83,9 @@ class Electiontests(Modeltests):
# Add an AdminGroup to election #2
admin = models.ElectionAdminGroup(
- election=obj,
- group_name='testers',
- )
+ election=obj,
+ group_name='testers',
+ )
self.session.add(admin)
self.session.commit()
self.assertNotEqual(obj, admin)
commit 35c46df93fb5060d72dddcaaa7e52b0c4d392b30
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:28:52 2014 +0200
Add unit-tests for the is_safe_url method
diff --git a/tests/test_flask.py b/tests/test_flask.py
index a70ae60..d18e633 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -154,6 +154,15 @@ class Flasktests(ModelFlasktests):
flask.g.fas_user, 2)
)
+ def test_is_safe_url(self):
+ """ Test the is_safe_url function. """
+ app = flask.Flask('fedora_elections')
+ with app.test_request_context():
+ self.assertFalse(
+ fedora_elections.is_safe_url('https://google.fr'))
+ self.assertTrue(
+ fedora_elections.is_safe_url('/admin/'))
+
def test_auth_login(self):
""" Test the auth_login function. """
app = flask.Flask('fedora_elections')
commit af87afe9652ce270e0574e9aabae0c03568117dd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:22:06 2014 +0200
Add unit-test testing the addition and removal of admin groups when editing an election
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index e770e19..fb408eb 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -385,6 +385,73 @@ class FlaskAdmintests(ModelFlasktests):
'<p>3 candidates found</p>'
in output.data)
+ # Edit Admin Group
+
+ # Check election before edit
+ output = self.app.get('/admin/test_election2/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<li>Number elected: 1</li>' in output.data)
+ self.assertTrue('<li>Admin groups: testers</li>' in output.data)
+
+ # Add a new admin group: sysadmin-main
+ data = {
+ 'alias': 'test_election2',
+ 'shortdesc': 'test election 2 shortdesc',
+ 'description': 'test election 2 description',
+ 'voting_type': 'range',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY - timedelta(days=7),
+ 'end_date': TODAY - timedelta(days=5),
+ 'seats_elected': '2',
+ 'candidates_are_fasusers': False,
+ 'embargoed': False,
+ 'admin_grp': 'testers, sysadmin-main',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election2/edit', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Election "test_election2" saved</li>'
+ in output.data)
+ # We edited the seats_elected from 1 to 2
+ self.assertTrue(
+ '<li>Number elected: 2</li>' in output.data)
+ self.assertTrue(
+ '<li>Admin groups: sysadmin-main, testers</li>'
+ in output.data)
+
+ # Remove an existing group: testers
+ data = {
+ 'alias': 'test_election2',
+ 'shortdesc': 'test election 2 shortdesc',
+ 'description': 'test election 2 description',
+ 'voting_type': 'range',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY - timedelta(days=7),
+ 'end_date': TODAY - timedelta(days=5),
+ 'seats_elected': '2',
+ 'candidates_are_fasusers': False,
+ 'embargoed': False,
+ 'admin_grp': 'sysadmin-main',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election2/edit', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Election "test_election2" saved</li>'
+ in output.data)
+ # We edited the seats_elected from 1 to 2
+ self.assertTrue(
+ '<li>Number elected: 2</li>' in output.data)
+ self.assertTrue(
+ '<li>Admin groups: sysadmin-main</li>'
+ in output.data)
+
def test_admin_add_candidate(self):
""" Test the admin_add_candidate function. """
user = FakeUser(
commit e7b40a47bd3c718541babb2c83ffdaea1b66bfee
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:21:56 2014 +0200
Sort the election's admin groups
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 6c31fe1..c766962 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -113,7 +113,7 @@ class Election(BASE):
@property
def admin_groups_list(self):
- return [grp.group_name for grp in self.admin_groups]
+ return sorted([grp.group_name for grp in self.admin_groups])
@property
def status(self):
commit 8a904fb47ece606f25ed8592f54feafd07fad754
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:16:28 2014 +0200
Test the is_election_admin when the user is and is not in an AdminGroup of that election
diff --git a/tests/test_flask.py b/tests/test_flask.py
index 44bdde7..a70ae60 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -138,6 +138,22 @@ class Flasktests(ModelFlasktests):
self.assertTrue(fedora_elections.is_election_admin(
flask.g.fas_user, 1))
+ self.setup_db()
+
+ with app.test_request_context():
+ flask.g.fas_user = FakeUser('testers')
+ # This is user is not an admin for election #1
+ self.assertFalse(
+ fedora_elections.is_election_admin(
+ flask.g.fas_user, 1)
+ )
+
+ # This is user is an admin for election #2
+ self.assertTrue(
+ fedora_elections.is_election_admin(
+ flask.g.fas_user, 2)
+ )
+
def test_auth_login(self):
""" Test the auth_login function. """
app = flask.Flask('fedora_elections')
commit 6298e3faea18438b30862c5d8656b60f42232ef7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:16:15 2014 +0200
Specify an admin group for the election #2
diff --git a/tests/test_election.py b/tests/test_election.py
index 6b88424..313a98e 100644
--- a/tests/test_election.py
+++ b/tests/test_election.py
@@ -81,6 +81,15 @@ class Electiontests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ # Add an AdminGroup to election #2
+ admin = models.ElectionAdminGroup(
+ election=obj,
+ group_name='testers',
+ )
+ self.session.add(admin)
+ self.session.commit()
+ self.assertNotEqual(obj, admin)
+
# Election open
obj = models.Election( # id:3
shortdesc='test election 3 shortdesc',
commit a02fba9a16b54c1c66ebc1187e18510343e6466c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:10:12 2014 +0200
Enhance coverage of the is_admin function
diff --git a/tests/test_flask.py b/tests/test_flask.py
index 544d01d..44bdde7 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -112,6 +112,12 @@ class Flasktests(ModelFlasktests):
fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'])
self.assertTrue(fedora_elections.is_admin(flask.g.fas_user))
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] = [
+ 'sysadmin-main', 'sysadmin-elections']
+ flask.g.fas_user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'])
+ self.assertTrue(fedora_elections.is_admin(flask.g.fas_user))
+
def test_is_election_admin(self):
""" Test the is_election_admin function. """
app = flask.Flask('fedora_elections')
commit 2903a283a7bf7930848c7063aaf38b030fe65a3a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:08:22 2014 +0200
Elections admins are now admins of every elections
diff --git a/tests/test_flask.py b/tests/test_flask.py
index 761ffa2..544d01d 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -129,7 +129,7 @@ class Flasktests(ModelFlasktests):
)
flask.g.fas_user = FakeUser(
fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'])
- self.assertFalse(fedora_elections.is_election_admin(
+ self.assertTrue(fedora_elections.is_election_admin(
flask.g.fas_user, 1))
def test_auth_login(self):
commit 29b4afe215e76601727fdb0adc4261d13a34e49c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:08:07 2014 +0200
Make the tests pass offline
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index 7503230..e770e19 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -52,7 +52,8 @@ class FlaskAdmintests(ModelFlasktests):
output = self.app.get('/admin/', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data)
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
user = FakeUser([], username='pingou')
with user_set(fedora_elections.APP, user):
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index b1c9fe4..ae72b46 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -52,7 +52,8 @@ class FlaskElectionstests(ModelFlasktests):
output = self.app.get('/vote/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data)
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
user = FakeUser([], username='pingou')
with user_set(fedora_elections.APP, user):
@@ -69,7 +70,8 @@ class FlaskElectionstests(ModelFlasktests):
output = self.app.get('/vote/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data)
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
self.setup_db()
@@ -133,7 +135,8 @@ class FlaskElectionstests(ModelFlasktests):
'/vote_range/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data)
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
self.setup_db()
@@ -245,7 +248,8 @@ class FlaskElectionstests(ModelFlasktests):
'/vote_range/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data)
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
self.setup_db()
@@ -328,7 +332,8 @@ class FlaskElectionstests(ModelFlasktests):
'/vote_simple/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data)
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
self.setup_db()
@@ -408,7 +413,8 @@ class FlaskElectionstests(ModelFlasktests):
'/vote_simple/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data)
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
self.setup_db()
commit 50a1d88f35a9451683f76c52a7467365ae0b9a17
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:00:34 2014 +0200
Implement the election admin groups
This allow the election admin creating the election to specify multiple
FAS groups which will be able to view the results before they are made
public without having to add them to the election admin group itself
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index a7e2f24..abb74a4 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -107,6 +107,8 @@ def is_election_admin(user, election_id):
return False
if not user.cla_done or len(user.groups) < 1:
return False
+ if is_admin(user):
+ return True
admingroups = [
group.group_name
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index 93a68fe..6d568c9 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -87,11 +87,12 @@ def admin_new_election():
time(23, 59, 59))
SESSION.add(election)
- admin = models.ElectionAdminGroup(
- election=election,
- group_name=APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
- )
- SESSION.add(admin)
+ for admin_grp in form.admin_grp.data.split(','):
+ admin = models.ElectionAdminGroup(
+ election=election,
+ group_name=admin_grp.strip(),
+ )
+ SESSION.add(admin)
SESSION.commit()
@@ -138,6 +139,26 @@ def admin_edit_election(election_alias):
form.candidates_are_fasusers.data = int(
form.candidates_are_fasusers.data)
form.populate_obj(election)
+
+ admin_groups = set(election.admin_groups_list)
+
+ new_groups = set(
+ [grp.strip() for grp in form.admin_grp.data.split(',')])
+
+ # Add the new groups
+ for admin_grp in new_groups.difference(admin_groups):
+ admin = models.ElectionAdminGroup(
+ election=election,
+ group_name=admin_grp,
+ )
+ SESSION.add(admin)
+
+ # Remove the groups that were removed with this edition
+ for admin_grp in admin_groups.difference(new_groups):
+ admingrp = models.ElectionAdminGroup.by_election_id_and_name(
+ SESSION, election.id, admin_grp)
+ SESSION.delete(admingrp)
+
SESSION.commit()
fedmsgshim.publish(
topic="election.edit",
@@ -150,6 +171,7 @@ def admin_edit_election(election_alias):
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
+ form.admin_grp.data = ', '.join(election.admin_groups_list)
return flask.render_template(
'admin/election_form.html',
form=form,
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index cca7176..c066f2d 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -59,6 +59,9 @@ class ElectionForm(wtf.Form):
embargoed = wtforms.BooleanField('Embargo results?', default=True)
+ admin_grp = wtforms.TextField(
+ 'Admin groups', [wtforms.validators.optional()])
+
def __init__(form, election_id=None, *args, **kwargs):
super(ElectionForm, form).__init__(*args, **kwargs)
form._election_id = election_id
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index c1cabfc..6c31fe1 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -112,6 +112,10 @@ class Election(BASE):
)
@property
+ def admin_groups_list(self):
+ return [grp.group_name for grp in self.admin_groups]
+
+ @property
def status(self):
now = datetime.utcnow()
if now.date() < self.start_date.date():
@@ -227,6 +231,21 @@ class ElectionAdminGroup(BASE):
cls.election_id == election_id
).all()
+ @classmethod
+ def by_election_id_and_name(cls, session, election_id, group_name):
+ """ Return the ElectionAdminGroup having a specific election_id
+ and group_name.
+ """
+ return session.query(
+ cls
+ ).filter(
+ cls.election_id == election_id
+ ).filter(
+ cls.group_name == group_name
+ ).first()
+
+
+
class Candidate(BASE):
__tablename__ = 'candidates'
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index 95eb06b..dd42f1c 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -21,6 +21,7 @@
{{ render_field_in_row(form.seats_elected) }}
{{ render_field_in_row(form.candidates_are_fasusers) }}
{{ render_field_in_row(form.embargoed) }}
+ {{ render_field_in_row(form.admin_grp) }}
</table>
</fieldset>
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index 3c63681..8726599 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -15,6 +15,7 @@
<li>End date: {{ election.end_date }}</li>
<li>Number elected: {{ election.seats_elected }}</li>
<li>Election type: {{ election.voting_type }}</li>
+ <li>Admin groups: {{ election.admin_groups_list | join(', ') }}</li>
</ul>
<h3>Candidates</h3>
{% if election.candidates.all() %}
commit 1229b32187733d3cdb9f71f23830ebbd03f4ec20
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 23:16:09 2014 +0200
Fix the reverse proxy
diff --git a/fedora_elections/proxy.py b/fedora_elections/proxy.py
index 40806cc..8e2e6b1 100644
--- a/fedora_elections/proxy.py
+++ b/fedora_elections/proxy.py
@@ -55,6 +55,10 @@ class ReverseProxied(object):
if path_info.startswith(script_name):
environ['PATH_INFO'] = path_info[len(script_name):]
+ server = environ.get('HTTP_X_FORWARDED_HOST', '')
+ if server:
+ environ['HTTP_HOST'] = server
+
scheme = environ.get('HTTP_X_SCHEME', '')
if scheme:
environ['wsgi.url_scheme'] = scheme
commit 297f60697854f664bb10e5efac3d7b9ee29225c2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 11:24:57 2014 +0200
Add elections.proxy and set elections as an application behind a reverse proxy
This will ensure that flask.redirect is using ``https`` where asked.
Fixes https://fedorahosted.org/fedora-infrastructure/ticket/4369 for elections
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 4e8878f..a7e2f24 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -38,7 +38,8 @@ from fedora.client import AuthError, AppError
from fedora.client.fas2 import AccountSystem
from flask.ext.fas_openid import FAS
-import fedmsgshim
+import fedora_elections.fedmsgshim
+import fedora_elections.proxy
APP = flask.Flask(__name__)
APP.config.from_object('fedora_elections.default_config')
@@ -47,6 +48,7 @@ if 'FEDORA_ELECTIONS_CONFIG' in os.environ: # pragma: no cover
# set up FAS
FAS = FAS(APP)
+APP.wsgi_app = fedora_elections.proxy.ReverseProxied(APP.wsgi_app)
# FAS for usernames.
FAS2 = AccountSystem(
diff --git a/fedora_elections/proxy.py b/fedora_elections/proxy.py
new file mode 100644
index 0000000..40806cc
--- /dev/null
+++ b/fedora_elections/proxy.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2014 Red Hat, Inc.
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions
+# of the GNU General Public License v.2, or (at your option) any later
+# version. This program is distributed in the hope that it will be
+# useful, but WITHOUT ANY WARRANTY expressed or implied, including the
+# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+# PURPOSE. See the GNU General Public License for more details. You
+# should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public
+# License and may only be used or replicated with the express permission
+# of Red Hat, Inc.
+#
+
+'''
+Makes elections an application behind a reverse proxy and thus ensure the
+redirects are using ``https``.
+
+Source: http://flask.pocoo.org/snippets/35/ by Peter Hansen
+'''
+
+
+class ReverseProxied(object):
+ '''Wrap the application in this middleware and configure the
+ front-end server to add these headers, to let you quietly bind
+ this to a URL other than / and to an HTTP scheme that is
+ different than what is used locally.
+
+ In nginx:
+ location /myprefix {
+ proxy_pass http://192.168.0.1:5001;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Scheme $scheme;
+ proxy_set_header X-Script-Name /myprefix;
+ }
+
+ :param app: the WSGI application
+ '''
+ def __init__(self, app):
+ self.app = app
+
+ def __call__(self, environ, start_response):
+ script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
+ if script_name:
+ environ['SCRIPT_NAME'] = script_name
+ path_info = environ['PATH_INFO']
+ if path_info.startswith(script_name):
+ environ['PATH_INFO'] = path_info[len(script_name):]
+
+ scheme = environ.get('HTTP_X_SCHEME', '')
+ if scheme:
+ environ['wsgi.url_scheme'] = scheme
+ return self.app(environ, start_response)
commit ea704ad587e140f9697d271f86b58d5d6883ba69
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 3 16:06:22 2014 +0200
Remove un-used script
diff --git a/testing b/testing
deleted file mode 100755
index 76f7c3e..0000000
--- a/testing
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-nosetests $*
-
commit c43ace49c3af55a32ae2a3e5378c53e9caa68256
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 18:31:53 2014 +0200
'##' is not allowed, and @abadger is right '# #' doesn't look right either
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index a97b40c..c1cabfc 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -41,7 +41,7 @@ def create_tables(db_url, alembic_ini=None, debug=False):
BASE.metadata.create_all(engine)
# engine.execute(collection_package_create_view(driver=engine.driver))
if db_url.startswith('sqlite:'): # pragma: no cover
- # # Ignore the warning about con_record
+ # Ignore the warning about con_record
# pylint: disable=W0613
def _fk_pragma_on_connect(dbapi_con, con_record):
''' Tries to enforce referential constraints on sqlite. '''
@@ -52,7 +52,7 @@ def create_tables(db_url, alembic_ini=None, debug=False):
# then, load the Alembic configuration and generate the
# version table, "stamping" it with the most recent rev:
- # # Ignore the warning missing alembic
+ # Ignore the warning missing alembic
# pylint: disable=F0401
from alembic.config import Config
from alembic import command
commit 0696a89321072071648875ee35eaf4d84912b5dc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 18:30:04 2014 +0200
Change comment as suggested by @abadger
diff --git a/tests/test_candidate.py b/tests/test_candidate.py
index 7a39f89..3c0d6ba 100644
--- a/tests/test_candidate.py
+++ b/tests/test_candidate.py
@@ -50,7 +50,9 @@ class Candidatetests(Modeltests):
elections.session = self.session
elections.test_init_election()
- # # Election #1
+ #
+ # Election #1
+ #
obj = models.Candidate( # id:1
election_id=1,
@@ -79,7 +81,9 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
- # # Election #3
+ #
+ # Election #3
+ #
obj = models.Candidate( # id:4
election_id=3,
@@ -108,7 +112,9 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
- # # Election #5
+ #
+ # Election #5
+ #
obj = models.Candidate( # id:7
election_id=5,
@@ -137,7 +143,9 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
- # # Election #4
+ #
+ # Election #4
+ #
obj = models.Candidate( # id:10
election_id=4,
commit e3d809cae0f77f0cb2cd7a1b9ba9d1df6fdc5514
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:51:45 2014 +0200
pep8 fixes on the tests
diff --git a/tests/__init__.py b/tests/__init__.py
index 81d4ccf..2881e5d 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
"""
(c) 2014 - Copyright Pierre-Yves Chibon
diff --git a/tests/test_candidate.py b/tests/test_candidate.py
index daabe85..7a39f89 100644
--- a/tests/test_candidate.py
+++ b/tests/test_candidate.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
"""
(c) 2014 - Copyright Pierre-Yves Chibon
@@ -50,7 +50,7 @@ class Candidatetests(Modeltests):
elections.session = self.session
elections.test_init_election()
- ## Election #1
+ # # Election #1
obj = models.Candidate( # id:1
election_id=1,
@@ -79,7 +79,7 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
- ## Election #3
+ # # Election #3
obj = models.Candidate( # id:4
election_id=3,
@@ -108,7 +108,7 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
- ## Election #5
+ # # Election #5
obj = models.Candidate( # id:7
election_id=5,
@@ -137,7 +137,7 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
- ## Election #4
+ # # Election #4
obj = models.Candidate( # id:10
election_id=4,
diff --git a/tests/test_election.py b/tests/test_election.py
index 99af3b8..6b88424 100644
--- a/tests/test_election.py
+++ b/tests/test_election.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
"""
(c) 2014 - Copyright Pierre-Yves Chibon
diff --git a/tests/test_flask.py b/tests/test_flask.py
index 6414512..761ffa2 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
"""
(c) 2014 - Copyright Pierre-Yves Chibon
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index 3ca0192..7503230 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
"""
(c) 2014 - Copyright Pierre-Yves Chibon
@@ -167,7 +167,6 @@ class FlaskAdmintests(ModelFlasktests):
'csrf_token': csrf_token,
}
-
output = self.app.post('/admin/new', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
@@ -202,7 +201,7 @@ class FlaskAdmintests(ModelFlasktests):
'input id="shortdesc" name="shortdesc" type="text"'
in output.data)
self.assertTrue(
- '<td class="error">End date must be later than start date.</td>'
+ 'class="error">End date must be later than start date.</td>'
in output.data)
# Invalid: alias already taken
@@ -231,7 +230,6 @@ class FlaskAdmintests(ModelFlasktests):
'<td class="error">There is already another election with '
'this alias.</td>' in output.data)
-
# Invalid: shortdesc already taken
data = {
'alias': 'new_election',
@@ -284,7 +282,6 @@ class FlaskAdmintests(ModelFlasktests):
self.assertTrue(
'<p>There are no candidates.</p>' in output.data)
-
def test_admin_edit_election(self):
""" Test the admin_edit_election function. """
user = FakeUser(
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index 06b0a7c..b1c9fe4 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
"""
(c) 2014 - Copyright Pierre-Yves Chibon
@@ -148,7 +148,7 @@ class FlaskElectionstests(ModelFlasktests):
user = FakeUser([], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get(
- '/vote_range/test_election5')#, follow_redirects=True)
+ '/vote_range/test_election5')
self.assertTrue(
'be redirected automatically to target URL: '
'<a href="/vote_simple/test_election5">' in output.data)
@@ -421,7 +421,8 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data, follow_redirects=True)
+ '/vote_simple/test_election5', data=data,
+ follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<li class="error">Invalid vote, this candidate is not '
@@ -434,7 +435,8 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data, follow_redirects=True)
+ '/vote_simple/test_election5', data=data,
+ follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<li class="error">Invalid vote, this candidate is not '
@@ -447,7 +449,8 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data, follow_redirects=True)
+ '/vote_simple/test_election5', data=data,
+ follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<li class="error">Invalid Ballot!</li' in output.data)
@@ -459,7 +462,8 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data, follow_redirects=True)
+ '/vote_simple/test_election5', data=data,
+ follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'class="message">Your vote has been recorded. Thank you!</'
diff --git a/tests/test_vote.py b/tests/test_vote.py
index ca2a206..09a4135 100644
--- a/tests/test_vote.py
+++ b/tests/test_vote.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
"""
(c) 2014 - Copyright Pierre-Yves Chibon
@@ -177,7 +177,6 @@ class Votetests(Modeltests):
obj = models.Candidate.by_id(self.session, 3)
self.assertEqual(obj.vote_count, 7)
-
def test_of_user_on_election(self):
""" Test the Vote.of_user_on_election function. """
self.test_init_vote()
commit d3b7e91a56dba5b5ed180f36f6dc45f4b60cb5e0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:49:19 2014 +0200
pep8 fixes on setup.py
diff --git a/setup.py b/setup.py
index 5058cbc..1821fe5 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@
Setup script
"""
-## These two lines are needed to run on EL6
+# These two lines are needed to run on EL6
__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
import pkg_resources
commit 2bbba085ea773df6281340fd44f20783bb3929be
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:49:12 2014 +0200
pep8 fixes on fedora_elections
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 2dc3360..4e8878f 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -149,7 +149,7 @@ def inject_variables():
version=__version__)
-### LIST VIEWS #############################################
+# LIST VIEWS #############################################
@APP.route('/')
def index():
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index 163bf03..93a68fe 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -205,7 +205,7 @@ def admin_add_multi_candidate(election_alias):
candidates_name = []
for entry in form.candidate.data.strip().split("|"):
candidate = entry.split("!")
- #No url
+ # No url
if len(candidate) == 1:
cand = models.Candidate(
election=election,
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
index fcdbea0..9a37e23 100644
--- a/fedora_elections/default_config.py
+++ b/fedora_elections/default_config.py
@@ -11,4 +11,3 @@ FAS_BASE_URL = 'https://admin.stg.fedoraproject.org/accounts/'
FAS_USERNAME = ''
FAS_PASSWORD = ''
FAS_CHECK_CERT = False
-
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 3548a7d..a97b40c 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -39,9 +39,9 @@ def create_tables(db_url, alembic_ini=None, debug=False):
"""
engine = create_engine(db_url, echo=debug)
BASE.metadata.create_all(engine)
- #engine.execute(collection_package_create_view(driver=engine.driver))
+ # engine.execute(collection_package_create_view(driver=engine.driver))
if db_url.startswith('sqlite:'): # pragma: no cover
- ## Ignore the warning about con_record
+ # # Ignore the warning about con_record
# pylint: disable=W0613
def _fk_pragma_on_connect(dbapi_con, con_record):
''' Tries to enforce referential constraints on sqlite. '''
@@ -52,7 +52,7 @@ def create_tables(db_url, alembic_ini=None, debug=False):
# then, load the Alembic configuration and generate the
# version table, "stamping" it with the most recent rev:
- ## Ignore the warning missing alembic
+ # # Ignore the warning missing alembic
# pylint: disable=F0401
from alembic.config import Config
from alembic import command
@@ -234,7 +234,10 @@ class Candidate(BASE):
id = sa.Column(sa.Integer, primary_key=True)
election_id = sa.Column(
sa.Integer,
- sa.ForeignKey('elections.id', ondelete='RESTRICT', onupdate='CASCADE'),
+ sa.ForeignKey(
+ 'elections.id',
+ ondelete='RESTRICT',
+ onupdate='CASCADE'),
nullable=False)
# FAS username if candidates_are_fasusers
name = sa.Column(sa.Unicode(150), nullable=False)
@@ -293,7 +296,10 @@ class Vote(BASE):
timestamp = sa.Column(sa.DateTime, nullable=False, default=safunc.now())
candidate_id = sa.Column(
sa.Integer,
- sa.ForeignKey('candidates.id', ondelete='RESTRICT', onupdate='CASCADE'),
+ sa.ForeignKey(
+ 'candidates.id',
+ ondelete='RESTRICT',
+ onupdate='CASCADE'),
nullable=False)
value = sa.Column(sa.Integer, nullable=False)
@@ -345,7 +351,7 @@ class Vote(BASE):
).filter(
cls.election_id == election_id
).filter(
- cls.value > 0
+ cls.value > 0
).count()
stats['n_votes'] = n_votes
commit 4fa078adcc84a6580f18adf886e05f52ed8128f3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:45:50 2014 +0200
Convert the boolean into integer as in the DB model (and required by postgresql)
This should fix: http://jenkins.cloud.fedoraproject.org/job/elections/3/
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index 7865a7c..163bf03 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -77,7 +77,7 @@ def admin_new_election():
seats_elected=form.seats_elected.data,
embargoed=int(form.embargoed.data),
voting_type=form.voting_type.data,
- candidates_are_fasusers=form.candidates_are_fasusers.data,
+ candidates_are_fasusers=int(form.candidates_are_fasusers.data),
fas_user=flask.g.fas_user.username,
)
@@ -135,6 +135,8 @@ def admin_edit_election(election_alias):
form = forms.ElectionForm(election.id, obj=election)
if form.validate_on_submit():
form.embargoed.data = int(form.embargoed.data)
+ form.candidates_are_fasusers.data = int(
+ form.candidates_are_fasusers.data)
form.populate_obj(election)
SESSION.commit()
fedmsgshim.publish(
commit 8532a1f4ca8ba5b23bc8b335da6475af8170ada1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:37:43 2014 +0200
Adjust the requirements file
diff --git a/requirements.txt b/requirements.txt
index 28de0aa..7bff0c9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,9 +1,11 @@
# Used for when working from a virtualenv.
# Use this file by running "$ pip install -r requirements.txt"
-flask>=0.10
+Flask>=0.10
+Flask-wtf
kitchen
python-fedora
-Flask-wtf
python-openid
python-openid-cla
python-openid-teams
+SQLAlchemy
+wtforms
commit 1f3974ed8270e45d000d2d60a346cc40d932b065
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:24:41 2014 +0200
Specify where to find the tests so that python setup.py test works
diff --git a/setup.py b/setup.py
index ea9b589..5058cbc 100644
--- a/setup.py
+++ b/setup.py
@@ -24,4 +24,5 @@ setup(
'python-openid', 'python-openid-teams', 'python-openid-cla',
'Flask-wtf', 'wtforms',
],
+ test_suite="tests",
)
commit 6e31ddc6bd4cf6c788e073b2f2e380ced02d7632
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:20:25 2014 +0200
Fix dependency in setup.py, the package is named kitchen not python-kitchen
diff --git a/setup.py b/setup.py
index 1b3a80a..ea9b589 100644
--- a/setup.py
+++ b/setup.py
@@ -20,7 +20,7 @@ setup(
packages=['fedora_elections'],
include_package_data=True,
install_requires=[
- 'Flask', 'SQLAlchemy>=0.7', 'python-fedora', 'python-kitchen',
+ 'Flask', 'SQLAlchemy>=0.7', 'python-fedora', 'kitchen',
'python-openid', 'python-openid-teams', 'python-openid-cla',
'Flask-wtf', 'wtforms',
],
commit 157e7ae869b9a82d9c2f86881dee58c8c55a71e8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 13:26:17 2014 +0200
Fix the database session for the forms, if we want the tests to run :)
diff --git a/tests/__init__.py b/tests/__init__.py
index cbc17d7..81d4ccf 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -42,8 +42,9 @@ sys.path.insert(0, os.path.join(os.path.dirname(
os.path.abspath(__file__)), '..'))
import fedora_elections
-import fedora_elections.elections
import fedora_elections.admin
+import fedora_elections.elections
+import fedora_elections.forms
from fedora_elections import models
@@ -129,8 +130,9 @@ class ModelFlasktests(Modeltests):
fedora_elections.APP.logger.setLevel(logging.CRITICAL)
fedora_elections.SESSION = self.session
- fedora_elections.elections.SESSION = self.session
fedora_elections.admin.SESSION = self.session
+ fedora_elections.elections.SESSION = self.session
+ fedora_elections.forms.SESSION = self.session
self.app = fedora_elections.APP.test_client()
commit 596ae1fdbd423e2a5f2697b0420efb59f78ba2e2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 13:25:43 2014 +0200
Add tests for the form fields validations when creating a new elections
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index ce66e44..3ca0192 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -64,7 +64,6 @@ class FlaskAdmintests(ModelFlasktests):
username='toshio')
with user_set(fedora_elections.APP, user):
output = self.app.get('/admin/')
- #print output.data
self.assertEqual(output.status_code, 200)
self.assertTrue('<h2>Election administration</h2>' in output.data)
@@ -89,6 +88,8 @@ class FlaskAdmintests(ModelFlasktests):
def test_admin_new_election(self):
""" Test the admin_new_election function. """
+ self.setup_db()
+
user = FakeUser(
fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
username='toshio')
@@ -151,6 +152,112 @@ class FlaskAdmintests(ModelFlasktests):
'<td class="error">This field is required.</td>'
in output.data)
+ # Invalid alias
+ data = {
+ 'alias': 'new',
+ 'shortdesc': 'new election shortdesc',
+ 'description': 'new election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=2),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': 2,
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ 'csrf_token': csrf_token,
+ }
+
+
+ output = self.app.post('/admin/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Create election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+ self.assertTrue(
+ '<td class="error">The alias cannot be <code>new</code>.</td>'
+ in output.data)
+
+ # Invalid: end_date earlier than start_date
+ data = {
+ 'alias': 'new_election',
+ 'shortdesc': 'new election shortdesc',
+ 'description': 'new election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=6),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': 2,
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/admin/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Create election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+ self.assertTrue(
+ '<td class="error">End date must be later than start date.</td>'
+ in output.data)
+
+ # Invalid: alias already taken
+ data = {
+ 'alias': 'test_election',
+ 'shortdesc': 'new election shortdesc',
+ 'description': 'new election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=6),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': 2,
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/admin/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Create election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+ self.assertTrue(
+ '<td class="error">There is already another election with '
+ 'this alias.</td>' in output.data)
+
+
+ # Invalid: shortdesc already taken
+ data = {
+ 'alias': 'new_election',
+ 'shortdesc': 'test election shortdesc',
+ 'description': 'new election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=6),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': 2,
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/admin/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Create election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+ self.assertTrue(
+ '<td class="error">There is already another election with '
+ 'this summary.</td>' in output.data)
+
# All good
data = {
'alias': 'new_election',
commit 772c496ee408ae3af649230e1e431f4be9cdc6d1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 13:24:49 2014 +0200
Add missing import and fix the validate_shortdesc to actually be used
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 43b228f..cca7176 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -1,5 +1,8 @@
-from flask.ext import wtf
+# -*- coding: utf-8 -*-
+
+import flask
import wtforms
+from flask.ext import wtf
from fedora_elections import SESSION
from fedora_elections.models import Election
@@ -60,7 +63,7 @@ class ElectionForm(wtf.Form):
super(ElectionForm, form).__init__(*args, **kwargs)
form._election_id = election_id
- def validate_summary(form, field):
+ def validate_shortdesc(form, field):
check = Election.search(SESSION, shortdesc=form.shortdesc.data)
if check:
if not (form._election_id and form._election_id == check[0].id):
commit c88d2021a1dd9540a88f2fdb5c42c684e1e66888
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 13:00:11 2014 +0200
Add a little bit of documentation in the admin tests
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index 9ad9315..ce66e44 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -101,6 +101,7 @@ class FlaskAdmintests(ModelFlasktests):
'input id="shortdesc" name="shortdesc" type="text"'
in output.data)
+ # No csrf provided
data = {
'alias': 'new_election',
'shortdesc': 'new election shortdesc',
@@ -125,6 +126,7 @@ class FlaskAdmintests(ModelFlasktests):
csrf_token = output.data.split(
'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+ # Description missing
data = {
'alias': 'new_election',
'shortdesc': 'new election shortdesc',
@@ -149,6 +151,7 @@ class FlaskAdmintests(ModelFlasktests):
'<td class="error">This field is required.</td>'
in output.data)
+ # All good
data = {
'alias': 'new_election',
'shortdesc': 'new election shortdesc',
commit 79cc86c7ee557bd98496bce2b7a8861f77e11ce2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:56:56 2014 +0200
Fix import and session for the unit-tests for the elections and admin controllers
diff --git a/tests/__init__.py b/tests/__init__.py
index 4ac5ec0..cbc17d7 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -42,6 +42,8 @@ sys.path.insert(0, os.path.join(os.path.dirname(
os.path.abspath(__file__)), '..'))
import fedora_elections
+import fedora_elections.elections
+import fedora_elections.admin
from fedora_elections import models
@@ -120,10 +122,16 @@ class ModelFlasktests(Modeltests):
super(ModelFlasktests, self).setUp()
fedora_elections.APP.config['TESTING'] = True
+ fedora_elections.APP.config[
+ 'FEDORA_ELECTIONS_ADMIN_GROUP'] = 'elections'
fedora_elections.APP.debug = True
fedora_elections.APP.logger.handlers = []
fedora_elections.APP.logger.setLevel(logging.CRITICAL)
fedora_elections.SESSION = self.session
+
+ fedora_elections.elections.SESSION = self.session
+ fedora_elections.admin.SESSION = self.session
+
self.app = fedora_elections.APP.test_client()
commit 9beb1ce45de8f2240b5b21808877a7775c7ce51e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:56:44 2014 +0200
Add unit-tests for the admin controller
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
new file mode 100644
index 0000000..9ad9315
--- /dev/null
+++ b/tests/test_flask_admin.py
@@ -0,0 +1,582 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections.admin test script
+"""
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import logging
+import unittest
+import sys
+import os
+
+from datetime import time
+from datetime import timedelta
+
+import flask
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+import fedora_elections
+from tests import ModelFlasktests, Modeltests, TODAY, FakeUser, user_set
+
+
+# pylint: disable=R0904
+class FlaskAdmintests(ModelFlasktests):
+ """ Flask application tests for the admin controller. """
+
+ def test_admin(self):
+ """ Test the admin function. """
+ output = self.app.get('/admin/')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get('/admin/', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data)
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/')
+ self.assertEqual(output.status_code, 403)
+
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/')
+ #print output.data
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>Election administration</h2>' in output.data)
+
+ def test_admin_view_election(self):
+ """ Test the admin_view_election function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/election_test/')
+ self.assertEqual(output.status_code, 404)
+
+ self.setup_db()
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<a href="/admin/test_election/edit">' in output.data)
+ self.assertTrue(
+ '<p>3 candidates found</p>' in output.data)
+
+ def test_admin_new_election(self):
+ """ Test the admin_new_election function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/new')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Create election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+
+ data = {
+ 'alias': 'new_election',
+ 'shortdesc': 'new election shortdesc',
+ 'description': 'new election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=2),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': '2',
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ }
+
+ output = self.app.post('/admin/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Create election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ data = {
+ 'alias': 'new_election',
+ 'shortdesc': 'new election shortdesc',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=2),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': '2',
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/admin/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Create election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+ self.assertTrue(
+ '<td class="error">This field is required.</td>'
+ in output.data)
+
+ data = {
+ 'alias': 'new_election',
+ 'shortdesc': 'new election shortdesc',
+ 'description': 'new election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=2),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': 2,
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/new', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Election "new_election" added</li>'
+ in output.data)
+ self.assertTrue(
+ '<a href="/admin/new_election/edit">' in output.data)
+ self.assertTrue(
+ '<p>There are no candidates.</p>' in output.data)
+
+
+ def test_admin_edit_election(self):
+ """ Test the admin_edit_election function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/edit')
+ self.assertEqual(output.status_code, 404)
+
+ self.setup_db()
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/edit')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Edit election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+
+ data = {
+ 'alias': 'test_election',
+ 'shortdesc': 'test election shortdesc',
+ 'description': 'test election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY - timedelta(days=10),
+ 'end_date': TODAY - timedelta(days=8),
+ 'seats_elected': '2',
+ 'candidates_are_fasusers': False,
+ 'embargoed': False,
+ }
+
+ output = self.app.post('/admin/test_election/edit', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Edit election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ data = {
+ 'alias': 'test_election',
+ 'shortdesc': 'test election shortdesc',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY - timedelta(days=10),
+ 'end_date': TODAY - timedelta(days=8),
+ 'seats_elected': '2',
+ 'candidates_are_fasusers': False,
+ 'embargoed': False,
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/admin/test_election/edit', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Edit election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+ self.assertTrue(
+ '<td class="error">This field is required.</td>'
+ in output.data)
+
+ # Check election before edit
+ output = self.app.get('/admin/test_election/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<p>3 candidates found</p>' in output.data)
+ self.assertTrue('<li>Number elected: 1</li>' in output.data)
+
+ data = {
+ 'alias': 'test_election',
+ 'shortdesc': 'test election shortdesc',
+ 'description': 'test election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY - timedelta(days=10),
+ 'end_date': TODAY - timedelta(days=8),
+ 'seats_elected': '2',
+ 'candidates_are_fasusers': False,
+ 'embargoed': False,
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/edit', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Election "test_election" saved</li>'
+ in output.data)
+ # We edited the seats_elected from 1 to 2
+ self.assertTrue(
+ '<li>Number elected: 2</li>' in output.data)
+ self.assertTrue(
+ '<p>3 candidates found</p>'
+ in output.data)
+
+ def test_admin_add_candidate(self):
+ """ Test the admin_add_candidate function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/new')
+ self.assertEqual(output.status_code, 404)
+
+ self.setup_db()
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/new')
+ self.assertEqual(output.status_code, 200)
+
+ self.assertTrue('<h2>Add candidate</h2>' in output.data)
+ self.assertTrue(
+ 'input id="name" name="name" type="text"' in output.data)
+
+ data = {
+ 'name': 'pingou',
+ 'url': '',
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>Add candidate</h2>' in output.data)
+ self.assertTrue(
+ 'input id="name" name="name" type="text"' in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ data = {
+ 'name': '',
+ 'url': '',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>Add candidate</h2>' in output.data)
+ self.assertTrue(
+ 'input id="name" name="name" type="text"' in output.data)
+ self.assertTrue(
+ '<td class="error">This field is required.</td>'
+ in output.data)
+
+ data = {
+ 'name': 'pingou',
+ 'url': '',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/new', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Candidate "pingou" saved</li>'
+ in output.data)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<p>4 candidates found</p>' in output.data)
+
+ def test_admin_add_multi_candidate(self):
+ """ Test the admin_add_multi_candidate function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/new/multi')
+ self.assertEqual(output.status_code, 404)
+
+ self.setup_db()
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/new/multi')
+ self.assertEqual(output.status_code, 200)
+
+ self.assertTrue('<h2>Add candidates</h2>' in output.data)
+ self.assertTrue(
+ 'input id="candidate" name="candidate" type="text"'
+ in output.data)
+
+ data = {
+ 'candidate': 'pingou',
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/new/multi', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>Add candidates</h2>' in output.data)
+ self.assertTrue(
+ 'input id="candidate" name="candidate" type="text"'
+ in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ data = {
+ 'candidate': '',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/new/multi', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>Add candidates</h2>' in output.data)
+ self.assertTrue(
+ 'input id="candidate" name="candidate" type="text"'
+ in output.data)
+ self.assertTrue(
+ '<td class="error">This field is required.</td>'
+ in output.data)
+
+ data = {
+ 'candidate': 'pingou|patrick!https://fedoraproject.org|'
+ 'shaiton!https://fedoraproject.org!test|sochotni',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/new/multi', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Added 3 candidates</li>'
+ in output.data)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<p>6 candidates found</p>' in output.data)
+
+ def test_admin_edit_candidate(self):
+ """ Test the admin_edit_candidate function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+
+ self.setup_db()
+
+ # Election does not exist
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test/candidates/1/edit')
+ self.assertEqual(output.status_code, 404)
+
+ # Candidate does not exist
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/100/edit')
+ self.assertEqual(output.status_code, 404)
+
+ # Candidate not in election
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/5/edit')
+ self.assertEqual(output.status_code, 404)
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/1/edit')
+ self.assertEqual(output.status_code, 200)
+
+ self.assertTrue('<h2>Edit candidate</h2>' in output.data)
+ self.assertTrue(
+ 'input id="name" name="name" type="text"' in output.data)
+
+ data = {
+ 'name': 'Toshio Kuratomi',
+ 'url': 'https://fedoraproject.org/wiki/User:Toshio',
+ }
+
+ self.assertTrue('<h2>Edit candidate</h2>' in output.data)
+ self.assertTrue(
+ 'input id="name" name="name" type="text"' in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ data = {
+ 'name': '',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/1/edit', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>Edit candidate</h2>' in output.data)
+ self.assertTrue(
+ 'input id="name" name="name" type="text"' in output.data)
+ self.assertTrue(
+ '<td class="error">This field is required.</td>'
+ in output.data)
+
+ # Check election before edit
+ output = self.app.get('/admin/test_election/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<p>3 candidates found</p>' in output.data)
+ self.assertTrue('<li>Number elected: 1</li>' in output.data)
+ self.assertFalse('<li>Toshio Kuratomi' in output.data)
+
+ data = {
+ 'name': 'Toshio Kuratomi',
+ 'url': 'https://fedoraproject.org/wiki/User:Toshio',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/1/edit', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Candidate "Toshio Kuratomi" saved</li>'
+ in output.data)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<p>3 candidates found</p>' in output.data)
+ self.assertTrue('<li>Toshio Kuratomi' in output.data)
+
+ def test_admin_delete_candidate(self):
+ """ Test the admin_delete_candidate function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+
+ self.setup_db()
+
+ # Election does not exist
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test/candidates/1/delete')
+ self.assertEqual(output.status_code, 404)
+
+ # Candidate does not exist
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/100/delete')
+ self.assertEqual(output.status_code, 404)
+
+ # Candidate not in election
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/5/delete')
+ self.assertEqual(output.status_code, 404)
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/1/delete')
+ self.assertEqual(output.status_code, 200)
+
+ self.assertTrue('<h2>Delete candidate</h2>' in output.data)
+ self.assertTrue(
+ 'p>Are you sure you want to delete candidate "Toshio"?</p'
+ in output.data)
+
+ output = self.app.post('/admin/test_election/candidates/1/delete')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>Delete candidate</h2>' in output.data)
+ self.assertTrue(
+ 'p>Are you sure you want to delete candidate "Toshio"?</p'
+ in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ # Try to delete while there are votes link to this candidates
+ data = {
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/1/delete', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>test election shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<li class="error">Could not delete this candidate. Is it '
+ 'already part of an election?</li>' in output.data)
+
+ # Check election before edit
+ output = self.app.get('/admin/test_election4/')
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<p>2 candidates found</p>' in output.data)
+ self.assertTrue('<li>Number elected: 1</li>' in output.data)
+ self.assertTrue('<li>Toshio' in output.data)
+ self.assertTrue(
+ '<h2>test election 4 shortdesc</h2>' in output.data)
+
+ # Delete one candidate
+ data = {
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election4/candidates/10/delete', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Candidate "Toshio" deleted</li>'
+ in output.data)
+ self.assertTrue(
+ '<h2>test election 4 shortdesc</h2>' in output.data)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<p>1 candidates found</p>' in output.data)
+ self.assertFalse('<li>Toshio' in output.data)
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(FlaskAdmintests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit 569a0e3d0cc8ed2e81d077b867ffbd5cdb70f33e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:55:46 2014 +0200
Update the behavior of the ForeinKey upon update or delete
And move the declaration of the relationships after the declaration of
the columns
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 8eb511e..3548a7d 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -208,11 +208,14 @@ class ElectionAdminGroup(BASE):
__tablename__ = 'electionadmins'
id = sa.Column(sa.Integer, primary_key=True)
- election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
- nullable=False)
+ election_id = sa.Column(
+ sa.Integer,
+ sa.ForeignKey('elections.id', ondelete='CASCADE', onupdate='CASCADE'),
+ nullable=False)
+ group_name = sa.Column(sa.Unicode(150), nullable=False)
+
election = relationship(
'Election', backref=backref('admin_groups', lazy='dynamic'))
- group_name = sa.Column(sa.Unicode(150), nullable=False)
@classmethod
def by_election_id(cls, session, election_id):
@@ -229,14 +232,17 @@ class Candidate(BASE):
__tablename__ = 'candidates'
id = sa.Column(sa.Integer, primary_key=True)
- election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
- nullable=False)
- election = relationship(
- 'Election', backref=backref('candidates', lazy='dynamic'))
+ election_id = sa.Column(
+ sa.Integer,
+ sa.ForeignKey('elections.id', ondelete='RESTRICT', onupdate='CASCADE'),
+ nullable=False)
# FAS username if candidates_are_fasusers
name = sa.Column(sa.Unicode(150), nullable=False)
url = sa.Column(sa.Unicode(250))
+ election = relationship(
+ 'Election', backref=backref('candidates', lazy='dynamic'))
+
def to_json(self):
''' Return a json representation of this object. '''
return dict(
@@ -261,13 +267,16 @@ class LegalVoter(BASE):
__tablename__ = 'legalvoters'
id = sa.Column(sa.Integer, primary_key=True)
- election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
- nullable=False)
+ election_id = sa.Column(
+ sa.Integer,
+ sa.ForeignKey('elections.id', ondelete='RESTRICT', onupdate='CASCADE'),
+ nullable=False)
+ group_name = sa.Column(sa.Unicode(150), nullable=False)
+
election = relationship(
'Election', backref=backref('legal_voters', lazy='dynamic'))
# special names:
# "cla + one" = cla_done + 1 non-cla group
- group_name = sa.Column(sa.Unicode(150), nullable=False)
class Vote(BASE):
@@ -280,15 +289,18 @@ class Vote(BASE):
id = sa.Column(sa.Integer, primary_key=True)
election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
nullable=False)
- election = relationship(
- 'Election', backref=backref('votes', lazy='dynamic'))
voter = sa.Column(sa.Unicode(150), nullable=False)
timestamp = sa.Column(sa.DateTime, nullable=False, default=safunc.now())
- candidate_id = sa.Column(sa.Integer, sa.ForeignKey('candidates.id'),
- nullable=False)
+ candidate_id = sa.Column(
+ sa.Integer,
+ sa.ForeignKey('candidates.id', ondelete='RESTRICT', onupdate='CASCADE'),
+ nullable=False)
+ value = sa.Column(sa.Integer, nullable=False)
+
+ election = relationship(
+ 'Election', backref=backref('votes', lazy='dynamic'))
candidate = relationship(
'Candidate', backref=backref('votes', lazy='dynamic'))
- value = sa.Column(sa.Integer, nullable=False)
@classmethod
def of_user_on_election(cls, session, user, election_id, count=False):
commit 55ce447a193d8739acf013deb8db4acda008e3cf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:55:16 2014 +0200
Handle error if someone tries to delete a candidate that already has votes
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index a943086..7865a7c 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -28,7 +28,7 @@ from datetime import datetime, time
from functools import wraps
import flask
-from sqlalchemy.orm.exc import NoResultFound
+from sqlalchemy.exc import SQLAlchemyError
from fedora_elections import fedmsgshim
from fedora_elections import forms
@@ -291,17 +291,26 @@ def admin_delete_candidate(election_alias, candidate_id):
form = forms.ConfirmationForm()
if form.validate_on_submit():
candidate_name = candidate.name
- SESSION.delete(candidate)
- SESSION.commit()
- flask.flash('Candidate "%s" deleted' % candidate_name)
- fedmsgshim.publish(
- topic="candidate.delete",
- msg=dict(
- agent=flask.g.fas_user.username,
- election=candidate.election.to_json(),
- candidate=candidate.to_json(),
+ try:
+ SESSION.delete(candidate)
+ SESSION.commit()
+ flask.flash('Candidate "%s" deleted' % candidate_name)
+ fedmsgshim.publish(
+ topic="candidate.delete",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=candidate.election.to_json(),
+ candidate=candidate.to_json(),
+ )
+ )
+ except SQLAlchemyError, err:
+ SESSION.rollback()
+ APP.logger.debug('Could not delete candidate')
+ APP.logger.exception(err)
+ flask.flash(
+ 'Could not delete this candidate. Is it already part of an '
+ 'election?', 'error'
)
- )
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
commit ba08e9cf4532b708e4af405b8a190696c9e9b90f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:54:38 2014 +0200
Add couple of candidates to the election #4
diff --git a/tests/test_candidate.py b/tests/test_candidate.py
index 5ade958..daabe85 100644
--- a/tests/test_candidate.py
+++ b/tests/test_candidate.py
@@ -137,6 +137,26 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ ## Election #4
+
+ obj = models.Candidate( # id:10
+ election_id=4,
+ name='Toshio',
+ url='https://fedoraproject.org/wiki/User:Toshio',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Candidate( # id:11
+ election_id=4,
+ name='Kevin',
+ url='https://fedoraproject.org/wiki/User:Kevin',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
def test_to_json(self):
""" Test the Candidate.to_json function. """
self.test_init_candidate()
commit b310504d02c5091a46c03559eb6682761aa23944
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:54:27 2014 +0200
Remove un-used imports
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 4e9d766..2dc3360 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -37,7 +37,6 @@ import flask
from fedora.client import AuthError, AppError
from fedora.client.fas2 import AccountSystem
from flask.ext.fas_openid import FAS
-from sqlalchemy.orm.exc import NoResultFound
import fedmsgshim
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 49386ff..8f1b1a3 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -28,7 +28,6 @@ from datetime import datetime, time
from functools import wraps
import flask
-from sqlalchemy.orm.exc import NoResultFound
from fedora_elections import fedmsgshim
from fedora_elections import forms
commit eaf911ccc37429682c79755be3098fea868c608c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 11:23:26 2014 +0200
Add unit-tests for the elections controller
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
new file mode 100644
index 0000000..06b0a7c
--- /dev/null
+++ b/tests/test_flask_elections.py
@@ -0,0 +1,548 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections.elections test script
+"""
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import logging
+import unittest
+import sys
+import os
+
+from datetime import time
+from datetime import timedelta
+
+import flask
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+import fedora_elections
+from tests import ModelFlasktests, Modeltests, TODAY, FakeUser, user_set
+
+
+# pylint: disable=R0904
+class FlaskElectionstests(ModelFlasktests):
+ """ Flask application tests for the elections controller. """
+
+ def test_vote_empty(self):
+ """ Test the vote function without data. """
+ output = self.app.get('/vote/test_election')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get('/vote/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data)
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/vote/test_election')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get('/vote/test_election', follow_redirects=True)
+ self.assertTrue(
+ '"message">The election, test_election, does not exist.</'
+ in output.data)
+
+ def test_vote(self):
+ """ Test the vote function. """
+ output = self.app.get('/vote/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+
+ output = self.app.get('/vote/test_election')
+ self.assertEqual(output.status_code, 302)
+
+ # Election closed and results open
+ output = self.app.get(
+ '/vote/test_election', follow_redirects=True)
+ self.assertTrue(
+ '<li class="message">This election is closed. You have been '
+ 'redirected to the election results.</li>' in output.data)
+
+ # Election closed and results are embargoed
+ output = self.app.get(
+ '/vote/test_election2', follow_redirects=True)
+ self.assertTrue(
+ '<li class="message">This election is closed. You have been '
+ 'redirected to the election results.</li>' in output.data)
+
+ # Election still pending
+ output = self.app.get(
+ '/vote/test_election4', follow_redirects=True)
+ self.assertTrue(
+ '<li class="message">Voting has not yet started, sorry.</li>'
+ in output.data)
+
+ # Election in progress
+ output = self.app.get('/vote/test_election3')
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Election in progress
+ output = self.app.get('/vote/test_election5')
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ user = FakeUser([], username='toshio')
+ with user_set(fedora_elections.APP, user):
+
+ # Election in progress
+ output = self.app.get(
+ '/vote/test_election3', follow_redirects=True)
+ self.assertTrue(
+ 'class="message">You have already voted in the election!</'
+ in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+
+ def test_vote_range(self):
+ """ Test the vote_range function - the preview part. """
+ output = self.app.get(
+ '/vote_range/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser([], username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/vote_range/test_election3', follow_redirects=True)
+ self.assertTrue(
+ 'class="message">You have already voted in the election!</'
+ in output.data)
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/vote_range/test_election5')#, follow_redirects=True)
+ self.assertTrue(
+ 'be redirected automatically to target URL: '
+ '<a href="/vote_simple/test_election5">' in output.data)
+
+ output = self.app.get(
+ '/vote_range/test_election3')
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Invalid candidate id
+ data = {
+ '9': 1,
+ '5': 3,
+ '6': 2,
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_range/test_election3', data=data)
+ self.assertEqual(output.status_code, 400)
+
+ # Invalid vote: too low
+ data = {
+ '4': -1,
+ '5': 3,
+ '6': 2,
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_range/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Invalid vote: too high
+ data = {
+ '4': 5,
+ '5': 3,
+ '6': 2,
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_range/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Invalid vote: Not numeric
+ data = {
+ '4': 'a',
+ '5': 3,
+ '6': 2,
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_range/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Valid input
+ data = {
+ '4': 1,
+ '5': 3,
+ '6': 2,
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_range/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="confirm" value="Submit" />'
+ in output.data)
+ self.assertTrue('<div class="vtmedcool">' in output.data)
+ self.assertTrue('<div class="vthot">' in output.data)
+ self.assertTrue('<div class="vtmedwarm">' in output.data)
+
+ def test_vote_range_process(self):
+ """ Test the vote_range function - the voting part. """
+ output = self.app.get(
+ '/vote_range/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ # Invalid candidate id
+ data = {
+ '9': 1,
+ '5': 3,
+ '6': 2,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post('/vote_range/test_election3', data=data)
+ self.assertEqual(output.status_code, 400)
+
+ # Invalid vote: too low
+ data = {
+ '4': -1,
+ '5': 3,
+ '6': 2,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_range/test_election3', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Invalid Ballot!</li' in output.data)
+
+ # Invalid vote: too high
+ data = {
+ '4': 5,
+ '5': 3,
+ '6': 2,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_range/test_election3', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Invalid Ballot!</li' in output.data)
+
+ # Invalid vote: Not numeric
+ data = {
+ '4': 'a',
+ '5': 3,
+ '6': 2,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_range/test_election3', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Invalid Ballot!</li' in output.data)
+
+ # Valid input
+ data = {
+ '4': 1,
+ '5': 3,
+ '6': 2,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_range/test_election3', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ 'class="message">Your vote has been recorded. Thank you!</'
+ in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
+
+ def test_vote_simple(self):
+ """ Test the vote_simple function - the preview part. """
+ output = self.app.get(
+ '/vote_simple/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser([], username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/vote_simple/test_election5', follow_redirects=True)
+ self.assertTrue(
+ 'class="message">You have already voted in the election!</'
+ in output.data)
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/vote_simple/test_election3')
+ self.assertTrue(
+ 'be redirected automatically to target URL: '
+ '<a href="/vote_range/test_election3">' in output.data)
+
+ output = self.app.get(
+ '/vote_simple/test_election5')
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Invalid vote: No candidate
+ data = {
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_simple/test_election5', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<li class="message">Please vote for a candidate</li>'
+ in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Invalid vote: Not numeric
+ data = {
+ 'candidate': 'a',
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_simple/test_election5', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Valid input
+ data = {
+ 'candidate': 7,
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_simple/test_election5', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="confirm" value="Submit" />'
+ in output.data)
+ self.assertTrue(
+ '<li class="message">Please confirm your vote!</li>'
+ in output.data)
+
+ def test_vote_simple_process(self):
+ """ Test the vote_simple function - the voting part. """
+ output = self.app.get(
+ '/vote_simple/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ # Invalid candidate id
+ data = {
+ 'candidate': 1,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_simple/test_election5', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="error">Invalid vote, this candidate is not '
+ 'listed for this election</li>' in output.data)
+
+ # Invalid vote: too low
+ data = {
+ 'candidate': -1,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_simple/test_election5', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="error">Invalid vote, this candidate is not '
+ 'listed for this election</li>' in output.data)
+
+ # Invalid vote: Not numeric
+ data = {
+ 'candidate': 'a',
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_simple/test_election5', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="error">Invalid Ballot!</li' in output.data)
+
+ # Valid input
+ data = {
+ 'candidate': 7,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_simple/test_election5', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ 'class="message">Your vote has been recorded. Thank you!</'
+ in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
+
+ def test_election_results(self):
+ """ Test the election_results function - the preview part. """
+ output = self.app.get(
+ '/results/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ 'class="message">The election, test_election, does not exist.</'
+ in output.data)
+
+ self.setup_db()
+
+ output = self.app.get('/results/test_election')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<th title="Number of votes received">Votes</th>'
+ in output.data)
+ self.assertTrue(
+ '<h3>Some statistics about this election</h3>'
+ in output.data)
+
+ output = self.app.get(
+ '/results/test_election2', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">We are sorry. The results for this '
+ 'election cannot be viewed because they are currently embargoed '
+ 'pending formal announcement.</li>' in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+
+ user = FakeUser([], username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/results/test_election2', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">We are sorry. The results for this '
+ 'election cannot be viewed because they are currently '
+ 'embargoed pending formal announcement.</li>'
+ in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/results/test_election2', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="warning">You are only seeing this page because '
+ 'you are an admin.</li>' in output.data)
+ self.assertTrue(
+ ' <li class="warning">The results for this election are '
+ 'currently embargoed pending formal announcement.</li>'
+ in output.data)
+ self.assertTrue(
+ '<th title="Number of votes received">Votes</th>'
+ in output.data)
+ self.assertTrue(
+ '<h3>Some statistics about this election</h3>'
+ in output.data)
+
+ user = FakeUser(
+ ['gitr2spec'],
+ username='kevin')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/results/test_election3', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Sorry but this election is in progress, '
+ 'you may not see its results already.</li>' in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(FlaskElectionstests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit 249339ffb694e3beb52abc004eef794d12260a4c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 11:23:11 2014 +0200
One more place where we do not want to cover FAS integration in the unit-tests
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 07dadb2..49386ff 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -316,7 +316,7 @@ def election_results(election_alias):
return safe_redirect_back()
usernamemap = {}
- if (election.candidates_are_fasusers):
+ if (election.candidates_are_fasusers): # pragma: no cover
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
commit 1814761bc9919108459bf40b5671aa9299845b79
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 07:45:08 2014 +0200
Ignore in the unit-test the possibility that get_valid_election does not return an election
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index a93cd96..07dadb2 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -290,7 +290,7 @@ def vote_simple(election_alias):
def election_results(election_alias):
election = get_valid_election(election_alias, ended=True)
- if not isinstance(election, models.Election):
+ if not isinstance(election, models.Election): # pragma: no cover
return election
elif election.embargoed:
commit 4695aa5e4027063969309f9f313f9a0a8a32abaf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 07:44:20 2014 +0200
Ignore FAS integration for the unit-tests
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index a14da8b..a93cd96 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -268,7 +268,7 @@ def vote_simple(election_alias):
next_action = 'confirm'
usernamemap = {}
- if (election.candidates_are_fasusers):
+ if (election.candidates_are_fasusers): # pragma: no cover
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
commit 56253d0b79b33ef07efcffd67f3b5cc18ca0c869
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 07:43:40 2014 +0200
Fix the simple voting, make sure the candidate submitted belong to the election
And handle case the candidate id submitted is not an integer
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 3f0fe19..a14da8b 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -228,7 +228,17 @@ def vote_simple(election_alias):
next_action = ''
form_values = flask.request.form.values()
if 'Submit' in form_values:
- candidate_id = int(flask.request.form['candidate'])
+ try:
+ candidates_id = [cand.id for cand in election.candidates]
+ candidate_id = int(flask.request.form['candidate'])
+ except ValueError:
+ flask.flash("Invalid Ballot!", "error")
+ return safe_redirect_back()
+ if candidate_id not in candidates_id:
+ flask.flash("Invalid vote, this candidate is not listed for "
+ "this election", "error")
+ return safe_redirect_back()
+
new_vote = models.Vote(
election_id=election.id,
voter=flask.g.fas_user.username,
commit dcf774637d0297f03f5bfe6b21f08ebd682815be
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 20:07:42 2014 +0200
Handle the case the id submitted is not an integer
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 73a6fbe..3f0fe19 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -244,8 +244,12 @@ def vote_simple(election_alias):
elif 'Preview' in form_values:
if ('candidate' in flask.request.form):
- flask.flash("Please confirm your vote!")
- candidate_id = int(flask.request.form['candidate'])
+ try:
+ candidate_id = int(flask.request.form['candidate'])
+ flask.flash("Please confirm your vote!")
+ except ValueError:
+ next_action = 'vote'
+ flask.flash("Invalid data")
else:
flask.flash("Please vote for a candidate")
next_action = 'vote'
commit cf7b7d0230697318d30a0db73bda30eaca54e34a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 17:47:40 2014 +0200
Ignore FAS integration for the unit-tests
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 37b84d0..73a6fbe 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -183,7 +183,7 @@ def vote_range(election_alias):
next_action = 'confirm'
usernamemap = {}
- if (election.candidates_are_fasusers):
+ if (election.candidates_are_fasusers): # pragma: no cover
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
commit 801b1e11c8ff2fe33cd43866b2bfaa069e63bdc5
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 17:47:35 2014 +0200
In case the input is invalid (not an int), the next_action is vote
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index d792802..37b84d0 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -176,9 +176,10 @@ def vote_range(election_alias):
uvotes[candidate.id] = 0
next_action = 'vote'
except ValueError:
+ next_action = 'vote'
flask.flash("Invalid data")
- if (next_action != 'vote'):
+ if next_action != 'vote':
next_action = 'confirm'
usernamemap = {}
commit bcd0b9f47aba6f5bc6090c1116bc002759828276
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 17:31:18 2014 +0200
Fix typo in the Preview (in case of wrong input)
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 26e476c..d792802 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -173,7 +173,7 @@ def vote_range(election_alias):
uvotes[candidate.id] = vote
else:
flask.flash("Invalid data2.")
- uvotes[c.id] = 0
+ uvotes[candidate.id] = 0
next_action = 'vote'
except ValueError:
flask.flash("Invalid data")
commit 71ea100fd4506769868129121d1a58f90a42b09c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:54:47 2014 +0200
Ignore case where we would not get a valid election object from get_valid_election
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 55522b2..26e476c 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -108,7 +108,7 @@ def vote(election_alias):
def vote_range(election_alias):
election = get_valid_election(election_alias)
- if not isinstance(election, models.Election):
+ if not isinstance(election, models.Election): # pragma: no cover
return election
if (election.voting_type == 'simple'):
@@ -205,7 +205,7 @@ def vote_range(election_alias):
def vote_simple(election_alias):
election = get_valid_election(election_alias)
- if not isinstance(election, models.Election):
+ if not isinstance(election, models.Election): # pragma: no cover
return election
if (election.voting_type == 'range'):
commit 939abe3e9d1cfaa000b8e6cb0e43ffb2c1938d5d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:54:03 2014 +0200
Adjust the candidate.id for the candidates that were added to elections #3
diff --git a/tests/test_vote.py b/tests/test_vote.py
index 4ee61e1..ca2a206 100644
--- a/tests/test_vote.py
+++ b/tests/test_vote.py
@@ -137,7 +137,7 @@ class Votetests(Modeltests):
obj = models.Vote( # id:1
election_id=5,
voter='toshio',
- candidate_id=4,
+ candidate_id=7,
value='1',
)
self.session.add(obj)
@@ -147,7 +147,7 @@ class Votetests(Modeltests):
obj = models.Vote( # id:2
election_id=5,
voter='toshio',
- candidate_id=5,
+ candidate_id=8,
value='1',
)
self.session.add(obj)
@@ -157,7 +157,7 @@ class Votetests(Modeltests):
obj = models.Vote( # id:4
election_id=5,
voter='kevin',
- candidate_id=6,
+ candidate_id=9,
value='1',
)
self.session.add(obj)
commit 1e20199ea95c8d9bee4afb438813cc3832c5b626
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:53:44 2014 +0200
Add candidates to the election #3 and adjust the candidate.id
diff --git a/tests/test_candidate.py b/tests/test_candidate.py
index 9883485..5ade958 100644
--- a/tests/test_candidate.py
+++ b/tests/test_candidate.py
@@ -79,10 +79,10 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
- ## Election #5
+ ## Election #3
obj = models.Candidate( # id:4
- election_id=5,
+ election_id=3,
name='Toshio',
url='https://fedoraproject.org/wiki/User:Toshio',
)
@@ -91,7 +91,7 @@ class Candidatetests(Modeltests):
self.assertNotEqual(obj, None)
obj = models.Candidate( # id:5
- election_id=5,
+ election_id=3,
name='Kevin',
url='https://fedoraproject.org/wiki/User:Kevin',
)
@@ -100,6 +100,35 @@ class Candidatetests(Modeltests):
self.assertNotEqual(obj, None)
obj = models.Candidate( # id:6
+ election_id=3,
+ name='Ralph',
+ url='https://fedoraproject.org/wiki/User:Ralph',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ ## Election #5
+
+ obj = models.Candidate( # id:7
+ election_id=5,
+ name='Toshio',
+ url='https://fedoraproject.org/wiki/User:Toshio',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Candidate( # id:8
+ election_id=5,
+ name='Kevin',
+ url='https://fedoraproject.org/wiki/User:Kevin',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Candidate( # id:9
election_id=5,
name='Ralph',
url='https://fedoraproject.org/wiki/User:Ralph',
commit 6f9cd7c2486d194355c0bcde9f835a7479db1946
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:43:57 2014 +0200
Ignore case where we don't know the election's voting type for the unit-tests
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 98a30ed..55522b2 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -97,7 +97,7 @@ def vote(election_alias):
return vote_range(election_alias)
elif election.voting_type == 'simple':
return vote_simple(election_alias)
- else:
+ else: # pragma: no cover
flask.flash(
'Unknown election voting type: %s' % election.voting_type)
return safe_redirect_back()
commit a38e0a6b983ea1ebe466e210f619921885e3bfda
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:39:03 2014 +0200
Adjust the flask tests to new election added
diff --git a/tests/test_flask.py b/tests/test_flask.py
index c4100c3..6414512 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -249,7 +249,7 @@ class Flasktests(ModelFlasktests):
output = self.app.get('/open')
self.assertEqual(output.status_code, 200)
self.assertTrue('<title>Fedora elections</title>' in output.data)
- self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 2 elections</h3>' in output.data)
self.assertTrue('<td>test election 3 shortdesc</td>' in output.data)
self.assertTrue('<a href="/vote_range/' in output.data)
self.assertTrue('<a href="/login">login</a>' in output.data)
@@ -259,7 +259,7 @@ class Flasktests(ModelFlasktests):
output = self.app.get('/open')
self.assertEqual(output.status_code, 200)
self.assertTrue('<title>Fedora elections</title>' in output.data)
- self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 2 elections</h3>' in output.data)
self.assertTrue('<a href="/vote_range/' in output.data)
self.assertTrue(
'<span class="text">logged in as </span>' in output.data)
@@ -270,7 +270,7 @@ class Flasktests(ModelFlasktests):
output = self.app.get('/open')
self.assertEqual(output.status_code, 200)
self.assertTrue('<title>Fedora elections</title>' in output.data)
- self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 2 elections</h3>' in output.data)
self.assertTrue(
'<span class="text">logged in as </span>' in output.data)
self.assertFalse('Vote now!' in output.data)
commit cb3c3c561a9c809c835488ec985eb4287fa3b809
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:38:40 2014 +0200
Add missing shebang to the runserver.py script
diff --git a/runserver.py b/runserver.py
index f5ce1d9..5f21658 100755
--- a/runserver.py
+++ b/runserver.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
import __main__
__main__.__requires__ = ['SQLAlchemy >= 0.7']
import pkg_resources
commit fb2a284710179a9ae204db7f646b3a36606c4885
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:38:19 2014 +0200
Add votes to the new election #5 with simple voting
diff --git a/tests/test_vote.py b/tests/test_vote.py
index 86908f7..4ee61e1 100644
--- a/tests/test_vote.py
+++ b/tests/test_vote.py
@@ -132,6 +132,38 @@ class Votetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ # Election 5
+
+ obj = models.Vote( # id:1
+ election_id=5,
+ voter='toshio',
+ candidate_id=4,
+ value='1',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:2
+ election_id=5,
+ voter='toshio',
+ candidate_id=5,
+ value='1',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:4
+ election_id=5,
+ voter='kevin',
+ candidate_id=6,
+ value='1',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
def test_vote_count_with_votes(self):
""" Test the Candidate.vote_count function with votes in. """
self.test_init_vote()
commit 19ccf601ef911bf82bee1e104f7cb9c2aa74da87
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:34:38 2014 +0200
Add candidates to the new election having simple voting
diff --git a/tests/test_candidate.py b/tests/test_candidate.py
index 2d7b47d..9883485 100644
--- a/tests/test_candidate.py
+++ b/tests/test_candidate.py
@@ -50,6 +50,8 @@ class Candidatetests(Modeltests):
elections.session = self.session
elections.test_init_election()
+ ## Election #1
+
obj = models.Candidate( # id:1
election_id=1,
name='Toshio',
@@ -77,6 +79,35 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ ## Election #5
+
+ obj = models.Candidate( # id:4
+ election_id=5,
+ name='Toshio',
+ url='https://fedoraproject.org/wiki/User:Toshio',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Candidate( # id:5
+ election_id=5,
+ name='Kevin',
+ url='https://fedoraproject.org/wiki/User:Kevin',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Candidate( # id:6
+ election_id=5,
+ name='Ralph',
+ url='https://fedoraproject.org/wiki/User:Ralph',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
def test_to_json(self):
""" Test the Candidate.to_json function. """
self.test_init_candidate()
commit 6906bfd5698d004e3d5b84d491810162971e098b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:33:55 2014 +0200
Add a new simple and open election and adjust the tests to this election
diff --git a/tests/test_election.py b/tests/test_election.py
index 73709d2..99af3b8 100644
--- a/tests/test_election.py
+++ b/tests/test_election.py
@@ -117,6 +117,24 @@ class Electiontests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ # Election - simple voting - Open
+ obj = models.Election( # id:5
+ shortdesc='test election 5 shortdesc',
+ alias='test_election5',
+ description='test election 5 description',
+ url='https://fedoraproject.org',
+ start_date=TODAY - timedelta(days=1),
+ end_date=TODAY + timedelta(days=3),
+ seats_elected=1,
+ embargoed=1,
+ voting_type='simple',
+ candidates_are_fasusers=0,
+ fas_user='skvidal',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
def test_get_election(self):
""" Test the Election.get function. """
self.test_init_election()
@@ -182,17 +200,19 @@ class Electiontests(Modeltests):
self.session, fas_user='skvidal')
self.assertNotEqual(obj, None)
self.assertNotEqual(obj, [])
- self.assertEqual(len(obj), 1)
+ self.assertEqual(len(obj), 2)
self.assertEqual(obj[0].description, 'test election 4 description')
+ self.assertEqual(obj[1].description, 'test election 5 description')
obj = models.Election.search(self.session)
self.assertNotEqual(obj, None)
self.assertNotEqual(obj, [])
- self.assertEqual(len(obj), 4)
+ self.assertEqual(len(obj), 5)
self.assertEqual(obj[0].description, 'test election 4 description')
- self.assertEqual(obj[1].description, 'test election 3 description')
- self.assertEqual(obj[2].description, 'test election 2 description')
- self.assertEqual(obj[3].description, 'test election description')
+ self.assertEqual(obj[1].description, 'test election 5 description')
+ self.assertEqual(obj[2].description, 'test election 3 description')
+ self.assertEqual(obj[3].description, 'test election 2 description')
+ self.assertEqual(obj[4].description, 'test election description')
def test_get_older_election(self):
""" Test the Election.get_older_election function. """
@@ -210,8 +230,9 @@ class Electiontests(Modeltests):
obj = models.Election.get_open_election(self.session, limit=TODAY)
self.assertNotEqual(obj, None)
self.assertNotEqual(obj, [])
- self.assertEqual(len(obj), 1)
- self.assertEqual(obj[0].shortdesc, 'test election 3 shortdesc')
+ self.assertEqual(len(obj), 2)
+ self.assertEqual(obj[0].shortdesc, 'test election 5 shortdesc')
+ self.assertEqual(obj[1].shortdesc, 'test election 3 shortdesc')
def test_get_next_election(self):
""" Test the Election.get_next_election function. """
commit 1846b24953a89e8bb9c5b2985c86dade1d9d1e39
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:21:46 2014 +0200
Add missing import in the elections controller
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 825cc82..98a30ed 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -34,8 +34,8 @@ from fedora_elections import fedmsgshim
from fedora_elections import forms
from fedora_elections import models
from fedora_elections import (
- APP, SESSION, is_authenticated, is_admin, is_safe_url,
- safe_redirect_back,
+ APP, SESSION, is_authenticated, is_admin, is_election_admin,
+ is_safe_url, safe_redirect_back,
)
commit 2b45fe92c9494f8aa9c9c7241c2a3c1bc74a11b8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:21:08 2014 +0200
Add missing spaces in flashed messages
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 5538357..825cc82 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -280,7 +280,7 @@ def election_results(election_alias):
elif election.embargoed:
if not hasattr(flask.g, 'fas_user') or not flask.g.fas_user:
- flask.flash("We are sorry. The results for this election"
+ flask.flash("We are sorry. The results for this election "
"cannot be viewed because they are currently "
"embargoed pending formal announcement.")
return safe_redirect_back()
@@ -295,7 +295,7 @@ def election_results(election_alias):
pass
else:
flask.flash(
- "We are sorry. The results for this election"
+ "We are sorry. The results for this election "
"cannot be viewed because they are currently "
"embargoed pending formal announcement.")
return safe_redirect_back()
commit 9bc0fb895495f88256a42dea9bae7791ded0e53d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:20:14 2014 +0200
Remove double spaces on a flashed message
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 184fd3c..5538357 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -55,7 +55,7 @@ def get_valid_election(election_alias, ended=False):
election = models.Election.get(SESSION, alias=election_alias)
if not election:
- flask.flash('The election, %s, does not exist.' % election_alias)
+ flask.flash('The election, %s, does not exist.' % election_alias)
return safe_redirect_back()
if election.status == 'Pending':
commit caa1712c8bc3014f2475c335ef3c4006f31201f1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:19:04 2014 +0200
Do not show voting page on election embargoed, these are just ended embargoed
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 0afb02e..184fd3c 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -62,7 +62,7 @@ def get_valid_election(election_alias, ended=False):
flask.flash('Voting has not yet started, sorry.')
return safe_redirect_back()
- elif not ended and election.status == 'Ended':
+ elif not ended and election.status in ('Ended', 'Embargoed'):
flask.flash(
'This election is closed. You have been redirected to the '
'election results.')
commit 72594ddd7175123e9af82c7fda02883b1796b418
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:05:14 2014 +0200
Update the auth_login method to remove duplicated checks
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 96a0d73..4e9d766 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -245,8 +245,6 @@ def open_elections():
@APP.route('/login', methods=('GET', 'POST'))
def auth_login():
- if flask.g.fas_user:
- return safe_redirect_back()
next_url = None
if 'next' in flask.request.args:
if is_safe_url(flask.request.args['next']):
@@ -256,7 +254,7 @@ def auth_login():
next_url = flask.url_for('.index')
if hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None:
- return flask.redirect(next_url)
+ return safe_redirect_back(next_url)
else:
return FAS.login(return_url=next_url)
commit 6639c107b827bfd3bda92077704094b96363011c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:56:03 2014 +0200
Port the flask applications to the ModelFlasktests class
diff --git a/tests/test_flask.py b/tests/test_flask.py
index 444d53f..c4100c3 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -23,7 +23,6 @@
__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
import pkg_resources
-import logging
import unittest
import sys
import os
@@ -37,32 +36,13 @@ sys.path.insert(0, os.path.join(os.path.dirname(
os.path.abspath(__file__)), '..'))
import fedora_elections
-from tests import Modeltests, TODAY, FakeUser, user_set
+from tests import ModelFlasktests, FakeUser, user_set
# pylint: disable=R0904
-class Flasktests(Modeltests):
+class Flasktests(ModelFlasktests):
""" Flask application tests. """
- def __setup_db(self):
- """ Add a calendar and some meetings so that we can play with
- something. """
- from test_vote import Votetests
- votes = Votetests('test_init_vote')
- votes.session = self.session
- votes.test_init_vote()
-
- def setUp(self):
- """ Set up the environnment, ran before every tests. """
- super(Flasktests, self).setUp()
-
- fedora_elections.APP.config['TESTING'] = True
- fedora_elections.APP.debug = True
- fedora_elections.APP.logger.handlers = []
- fedora_elections.APP.logger.setLevel(logging.CRITICAL)
- fedora_elections.SESSION = self.session
- self.app = fedora_elections.APP.test_client()
-
def test_index_empty(self):
""" Test the index function. """
output = self.app.get('/')
@@ -83,7 +63,7 @@ class Flasktests(Modeltests):
def test_index_filled(self):
""" Test the index function. """
- self.__setup_db()
+ self.setup_db()
output = self.app.get('/')
self.assertEqual(output.status_code, 200)
self.assertTrue('<title>Fedora elections</title>' in output.data)
@@ -164,7 +144,7 @@ class Flasktests(Modeltests):
output = self.app.get('/login?next=http://localhost/')
self.assertEqual(output.status_code, 200)
- self.__setup_db()
+ self.setup_db()
user = FakeUser([], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get('/login', follow_redirects=True)
@@ -194,7 +174,7 @@ class Flasktests(Modeltests):
def test_about_election(self):
""" Test the about_election function. """
- self.__setup_db()
+ self.setup_db()
output = self.app.get('/about/blah')
self.assertEqual(output.status_code, 302)
@@ -237,7 +217,7 @@ class Flasktests(Modeltests):
self.assertTrue('<title>Fedora elections</title>' in output.data)
self.assertTrue('<h2>Elections</h2>' in output.data)
- self.__setup_db()
+ self.setup_db()
output = self.app.get('/archives')
self.assertEqual(output.status_code, 200)
@@ -264,7 +244,7 @@ class Flasktests(Modeltests):
self.assertTrue('<title>Fedora elections</title>' in output.data)
self.assertTrue('<h2>Elections</h2>' in output.data)
- self.__setup_db()
+ self.setup_db()
output = self.app.get('/open')
self.assertEqual(output.status_code, 200)
commit ba32398615a4706ff92921d89402390beefed0d0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:55:43 2014 +0200
Add a generic unit-test class for the Flask tests
diff --git a/tests/__init__.py b/tests/__init__.py
index 59e94fe..4ac5ec0 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -24,6 +24,7 @@
__requires__ = ['SQLAlchemy >= 0.7']
import pkg_resources
+import logging
import unittest
import sys
import os
@@ -40,6 +41,7 @@ from sqlalchemy.orm import scoped_session
sys.path.insert(0, os.path.join(os.path.dirname(
os.path.abspath(__file__)), '..'))
+import fedora_elections
from fedora_elections import models
@@ -102,6 +104,29 @@ class Modeltests(unittest.TestCase):
requests.get('%s/clean/%s' % (FAITOUT_URL, db_name))
+class ModelFlasktests(Modeltests):
+ """ Model flask application tests. """
+
+ def setup_db(self):
+ """ Add a calendar and some meetings so that we can play with
+ something. """
+ from test_vote import Votetests
+ votes = Votetests('test_init_vote')
+ votes.session = self.session
+ votes.test_init_vote()
+
+ def setUp(self):
+ """ Set up the environnment, ran before every tests. """
+ super(ModelFlasktests, self).setUp()
+
+ fedora_elections.APP.config['TESTING'] = True
+ fedora_elections.APP.debug = True
+ fedora_elections.APP.logger.handlers = []
+ fedora_elections.APP.logger.setLevel(logging.CRITICAL)
+ fedora_elections.SESSION = self.session
+ self.app = fedora_elections.APP.test_client()
+
+
class FakeGroup(object):
""" Fake object used to make the FakeUser object closer to the
expectations.
commit ebdd775278cd664ae9f6e69e1f5d6ed66319851f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:23:40 2014 +0200
Ignore fedmsg in the unit-tests
diff --git a/fedora_elections/fedmsgshim.py b/fedora_elections/fedmsgshim.py
index 2e13398..785fc90 100644
--- a/fedora_elections/fedmsgshim.py
+++ b/fedora_elections/fedmsgshim.py
@@ -9,7 +9,7 @@ send messages. If it is not installed, it will return silently.
import warnings
-def publish(*args, **kwargs):
+def publish(*args, **kwargs): # pragma: no cover
try:
import fedmsg
fedmsg.publish(*args, **kwargs)
commit 64ee446ad9f78a875027ea84b2422d660cd8c858
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:22:05 2014 +0200
Add unit-tests for the generic flask endpoints
diff --git a/tests/test_flask.py b/tests/test_flask.py
new file mode 100644
index 0000000..444d53f
--- /dev/null
+++ b/tests/test_flask.py
@@ -0,0 +1,301 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections.model.Election test script
+"""
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import logging
+import unittest
+import sys
+import os
+
+from datetime import time
+from datetime import timedelta
+
+import flask
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+import fedora_elections
+from tests import Modeltests, TODAY, FakeUser, user_set
+
+
+# pylint: disable=R0904
+class Flasktests(Modeltests):
+ """ Flask application tests. """
+
+ def __setup_db(self):
+ """ Add a calendar and some meetings so that we can play with
+ something. """
+ from test_vote import Votetests
+ votes = Votetests('test_init_vote')
+ votes.session = self.session
+ votes.test_init_vote()
+
+ def setUp(self):
+ """ Set up the environnment, ran before every tests. """
+ super(Flasktests, self).setUp()
+
+ fedora_elections.APP.config['TESTING'] = True
+ fedora_elections.APP.debug = True
+ fedora_elections.APP.logger.handlers = []
+ fedora_elections.APP.logger.setLevel(logging.CRITICAL)
+ fedora_elections.SESSION = self.session
+ self.app = fedora_elections.APP.test_client()
+
+ def test_index_empty(self):
+ """ Test the index function. """
+ output = self.app.get('/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertFalse('<h3>Current elections</h3>' in output.data)
+ self.assertFalse('<h3>Next' in output.data)
+ self.assertFalse('<h3>Last' in output.data)
+ self.assertTrue('<a href="/login">login</a>' in output.data)
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue(
+ '<span class="text">logged in as </span>' in output.data)
+
+ def test_index_filled(self):
+ """ Test the index function. """
+ self.__setup_db()
+ output = self.app.get('/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
+ self.assertTrue('<a href="/login">login</a>' in output.data)
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
+ self.assertTrue('<a href="/vote_range/' in output.data)
+ self.assertTrue(
+ '<span class="text">logged in as </span>' in output.data)
+ self.assertTrue('Vote now!' in output.data)
+
+ user = FakeUser([], username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
+ self.assertTrue(
+ '<span class="text">logged in as </span>' in output.data)
+ self.assertFalse('Vote now!' in output.data)
+
+ def test_is_admin(self):
+ """ Test the is_admin function. """
+ app = flask.Flask('fedora_elections')
+
+ with app.test_request_context():
+ flask.g.fas_user = None
+ self.assertFalse(fedora_elections.is_admin(flask.g.fas_user))
+
+ flask.g.fas_user = FakeUser()
+ self.assertFalse(fedora_elections.is_admin(flask.g.fas_user))
+
+ flask.g.fas_user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'])
+ self.assertTrue(fedora_elections.is_admin(flask.g.fas_user))
+
+ def test_is_election_admin(self):
+ """ Test the is_election_admin function. """
+ app = flask.Flask('fedora_elections')
+
+ with app.test_request_context():
+ flask.g.fas_user = None
+ self.assertFalse(
+ fedora_elections.is_election_admin(
+ flask.g.fas_user, 1)
+ )
+ flask.g.fas_user = FakeUser()
+ self.assertFalse(
+ fedora_elections.is_election_admin(
+ flask.g.fas_user, 1)
+ )
+ flask.g.fas_user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'])
+ self.assertFalse(fedora_elections.is_election_admin(
+ flask.g.fas_user, 1))
+
+ def test_auth_login(self):
+ """ Test the auth_login function. """
+ app = flask.Flask('fedora_elections')
+
+ with app.test_request_context():
+ flask.g.fas_user = FakeUser(['gitr2spec'])
+ output = self.app.get('/login')
+ self.assertEqual(output.status_code, 200)
+
+ output = self.app.get('/login?next=http://localhost/')
+ self.assertEqual(output.status_code, 200)
+
+ self.__setup_db()
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/login', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+
+ def test_auth_logout(self):
+ """ Test the auth_logout function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'])
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/logout', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue(
+ '<li class="message">You have been logged out</li>'
+ in output.data)
+
+ user = None
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/logout', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertFalse(
+ '<li class="message">You have been logged out</li>'
+ in output.data)
+
+ def test_about_election(self):
+ """ Test the about_election function. """
+ self.__setup_db()
+
+ output = self.app.get('/about/blah')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get('/about/blah', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">The election, blah, does not exist.</li>'
+ in output.data)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
+
+ output = self.app.get('/about/test_election')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>Election Information</title>' in output.data)
+ self.assertTrue(
+ '<small><a href="https://fedoraproject.org/wiki/User:Toshio">'
+ '[info]</a></small>' in output.data)
+ self.assertTrue(
+ '<small><a href="https://fedoraproject.org/wiki/User:Ralph">'
+ '[info]</a></small>' in output.data)
+ self.assertTrue(
+ '<a href="/results/test_election">Show Results!</a>'
+ in output.data)
+ self.assertTrue('<a href="/login">login</a>' in output.data)
+
+ def test_archived_election(self):
+ """ Test the archived_elections function. """
+ output = self.app.get('/archives')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get('/archives', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">There are no archived elections.</li>'
+ in output.data)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h2>Elections</h2>' in output.data)
+
+ self.__setup_db()
+
+ output = self.app.get('/archives')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h2>Past Elections</h2>' in output.data)
+ self.assertTrue(
+ '<td>test election 2 description</td>' in output.data)
+ self.assertTrue(
+ '<td>test election description</td>' in output.data)
+ self.assertEqual(output.data.count('<a href="results/'), 2)
+ self.assertEqual(output.data.count('<a href="about/'), 2)
+ self.assertTrue('<a href="/login">login</a>' in output.data)
+
+ def test_open_elections(self):
+ """ Test the open_elections function. """
+ output = self.app.get('/open')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get('/open', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">There are no open elections.</li>'
+ in output.data)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h2>Elections</h2>' in output.data)
+
+ self.__setup_db()
+
+ output = self.app.get('/open')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<td>test election 3 shortdesc</td>' in output.data)
+ self.assertTrue('<a href="/vote_range/' in output.data)
+ self.assertTrue('<a href="/login">login</a>' in output.data)
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/open')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<a href="/vote_range/' in output.data)
+ self.assertTrue(
+ '<span class="text">logged in as </span>' in output.data)
+ self.assertTrue('Vote now!' in output.data)
+
+ user = FakeUser([], username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/open')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue(
+ '<span class="text">logged in as </span>' in output.data)
+ self.assertFalse('Vote now!' in output.data)
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(Flasktests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit a108c762c6e76d939a7e43d418ae7735546a4b8d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:21:11 2014 +0200
Add some more votes on the election #3
This way we have someone that voted on an open election
diff --git a/tests/test_vote.py b/tests/test_vote.py
index 9aaccac..86908f7 100644
--- a/tests/test_vote.py
+++ b/tests/test_vote.py
@@ -100,18 +100,51 @@ class Votetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ # Election 3
+
+ obj = models.Vote( # id:1
+ election_id=3,
+ voter='toshio',
+ candidate_id=1,
+ value='3',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:2
+ election_id=3,
+ voter='toshio',
+ candidate_id=2,
+ value='3',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:4
+ election_id=3,
+ voter='toshio',
+ candidate_id=3,
+ value='3',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
def test_vote_count_with_votes(self):
""" Test the Candidate.vote_count function with votes in. """
self.test_init_vote()
obj = models.Candidate.get(self.session, 2)
- self.assertEqual(obj.vote_count, 5)
+ self.assertEqual(obj.vote_count, 8)
+
+ obj = models.Candidate.get(self.session, 1)
+ self.assertEqual(obj.vote_count, 6)
obj = models.Candidate.by_id(self.session, 3)
- self.assertEqual(obj.vote_count, 4)
+ self.assertEqual(obj.vote_count, 7)
- obj = models.Candidate.get(self.session, 1)
- self.assertEqual(obj.vote_count, 3)
def test_of_user_on_election(self):
""" Test the Vote.of_user_on_election function. """
commit b750e792e6c53d7e0e60ef6d61895f5bd71b44d7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:20:50 2014 +0200
Ignore some other lines in the unit-tests coverage report
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 53ecf9b..96a0d73 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -127,10 +127,10 @@ def is_safe_url(target):
def safe_redirect_back(next=None, fallback=('index', {})):
''' Safely redirect the user to its previous page. '''
targets = []
- if next:
+ if next: # pragma: no cover
targets.append(next)
if 'next' in flask.request.args and \
- flask.request.args['next']:
+ flask.request.args['next']: # pragma: no cover
targets.append(flask.request.args['next'])
targets.append(flask.url_for(fallback[0], **fallback[1]))
for target in targets:
@@ -187,7 +187,7 @@ def about_election(election_alias):
return safe_redirect_back()
usernamemap = {}
- if (election.candidates_are_fasusers):
+ if (election.candidates_are_fasusers): # pragma: no cover
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
commit ca7698490bfb39222b2ec0c1c37225f8c03fd1d2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 14:44:27 2014 +0200
Add missing docstrings
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 966eb37..53ecf9b 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -117,6 +117,7 @@ def is_election_admin(user, election_id):
def is_safe_url(target):
+ ''' Check is a url is safe to use or not. '''
ref_url = urlparse(flask.request.host_url)
test_url = urlparse(urljoin(flask.request.host_url, target))
return test_url.scheme in ('http', 'https') and \
@@ -124,6 +125,7 @@ def is_safe_url(target):
def safe_redirect_back(next=None, fallback=('index', {})):
+ ''' Safely redirect the user to its previous page. '''
targets = []
if next:
targets.append(next)
commit 9d1b876057a999690572690dad486f9699a2603c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 14:42:39 2014 +0200
Ignore reading the config file specified by environment variable
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 9873635..966eb37 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -43,7 +43,7 @@ import fedmsgshim
APP = flask.Flask(__name__)
APP.config.from_object('fedora_elections.default_config')
-if 'FEDORA_ELECTIONS_CONFIG' in os.environ:
+if 'FEDORA_ELECTIONS_CONFIG' in os.environ: # pragma: no cover
APP.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
# set up FAS
commit ae9feacd4c045fc03ee8664993c18db210e0e30b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:28:29 2014 +0200
Ignore the .coverage file created when running the tests with coverage
diff --git a/.gitignore b/.gitignore
index da17908..fc2dc27 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
*.egg-info
build/
dist/
+.coverage
commit 8c8dac6c0d39a74500284f5f1f71f4bac1ac8a3c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:27:54 2014 +0200
Add the runtest.sh script for easy testings of the project
diff --git a/runtests.sh b/runtests.sh
new file mode 100755
index 0000000..47f97e4
--- /dev/null
+++ b/runtests.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+PYTHONPATH=fedora_elections ./nosetests \
+--with-coverage --cover-erase --cover-package=fedora_elections $*
commit f1c7aa44b2d4d67b2a926f04844475475d49b512
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:27:32 2014 +0200
Add a local nosetests script ensuring we run the proper version of the libraries
diff --git a/nosetests b/nosetests
new file mode 100755
index 0000000..42c36f1
--- /dev/null
+++ b/nosetests
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# EASY-INSTALL-ENTRY-SCRIPT: 'nose==1.3.0','console_scripts','nosetests'
+__requires__ = ['nose==1.3.0', 'flask>=0.10']
+import sys
+from pkg_resources import load_entry_point
+
+if __name__ == '__main__':
+ sys.exit(
+ load_entry_point('nose==1.3.0', 'console_scripts', 'nosetests')()
+ )
commit fbbf712917b8fba53975c6ed4a39d93965c67849
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:27:17 2014 +0200
Add unit-tests for the Vote object
diff --git a/tests/test_vote.py b/tests/test_vote.py
new file mode 100644
index 0000000..9aaccac
--- /dev/null
+++ b/tests/test_vote.py
@@ -0,0 +1,150 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections.model.Vote test script
+"""
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import unittest
+import sys
+import os
+
+from datetime import time
+from datetime import timedelta
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+from fedora_elections import models
+from tests import Modeltests, TODAY
+from test_candidate import Candidatetests
+
+
+# pylint: disable=R0904
+class Votetests(Modeltests):
+ """ Vote tests. """
+
+ session = None
+
+ def test_init_vote(self):
+ """ Test the Vote init function. """
+ candidates = Candidatetests('test_init_candidate')
+ candidates.session = self.session
+ candidates.test_init_candidate()
+
+ obj = models.Vote( # id:1
+ election_id=1,
+ voter='toshio',
+ candidate_id=1,
+ value='3',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:2
+ election_id=1,
+ voter='toshio',
+ candidate_id=2,
+ value='3',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:3
+ election_id=1,
+ voter='toshio',
+ candidate_id=3,
+ value='3',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:4
+ election_id=1,
+ voter='ralph',
+ candidate_id=2,
+ value=2,
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:5
+ election_id=1,
+ voter='ralph',
+ candidate_id=3,
+ value=1,
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ def test_vote_count_with_votes(self):
+ """ Test the Candidate.vote_count function with votes in. """
+ self.test_init_vote()
+
+ obj = models.Candidate.get(self.session, 2)
+ self.assertEqual(obj.vote_count, 5)
+
+ obj = models.Candidate.by_id(self.session, 3)
+ self.assertEqual(obj.vote_count, 4)
+
+ obj = models.Candidate.get(self.session, 1)
+ self.assertEqual(obj.vote_count, 3)
+
+ def test_of_user_on_election(self):
+ """ Test the Vote.of_user_on_election function. """
+ self.test_init_vote()
+
+ obj = models.Vote.of_user_on_election(
+ self.session, 'toshio', 1, count=True)
+ self.assertEqual(obj, 3)
+
+ obj = models.Vote.of_user_on_election(
+ self.session, 'toshio', 1, count=False)
+ self.assertEqual(len(obj), 3)
+ self.assertEqual(obj[0].voter, 'toshio')
+ self.assertEqual(obj[0].candidate.name, 'Toshio')
+ self.assertEqual(obj[1].voter, 'toshio')
+ self.assertEqual(obj[1].candidate.name, 'Kevin')
+ self.assertEqual(obj[2].voter, 'toshio')
+ self.assertEqual(obj[2].candidate.name, 'Ralph')
+
+ def test_get_election_stats(self):
+ """ Test the get_election_stats function. """
+ self.test_init_vote()
+
+ obj = models.Vote.get_election_stats(self.session, 1)
+ self.assertEqual(obj['n_voters'], 2)
+ self.assertEqual(obj['n_votes'], 5)
+ self.assertEqual(
+ obj['candidate_voters'],
+ {'Toshio': 1, 'Ralph': 2, 'Kevin': 2}
+ )
+ self.assertEqual(obj['n_candidates'], 3)
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(Votetests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit 8b2b498eaf456c9c4c50576611bb9df5eb2aa863
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:27:03 2014 +0200
Add unit-tests for the Candidate object
diff --git a/tests/test_candidate.py b/tests/test_candidate.py
new file mode 100644
index 0000000..2d7b47d
--- /dev/null
+++ b/tests/test_candidate.py
@@ -0,0 +1,114 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections.model.Candidate test script
+"""
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import unittest
+import sys
+import os
+
+from datetime import time
+from datetime import timedelta
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+from fedora_elections import models
+from tests import Modeltests, TODAY
+from test_election import Electiontests
+
+
+# pylint: disable=R0904
+class Candidatetests(Modeltests):
+ """ Candidate tests. """
+
+ session = None
+
+ def test_init_candidate(self):
+ """ Test the Candidate init function. """
+ elections = Electiontests('test_init_election')
+ elections.session = self.session
+ elections.test_init_election()
+
+ obj = models.Candidate( # id:1
+ election_id=1,
+ name='Toshio',
+ url='https://fedoraproject.org/wiki/User:Toshio',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Candidate( # id:2
+ election_id=1,
+ name='Kevin',
+ url='https://fedoraproject.org/wiki/User:Kevin',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Candidate( # id:3
+ election_id=1,
+ name='Ralph',
+ url='https://fedoraproject.org/wiki/User:Ralph',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ def test_to_json(self):
+ """ Test the Candidate.to_json function. """
+ self.test_init_candidate()
+ obj = models.Candidate.by_id(self.session, 3)
+ self.assertEqual(
+ obj.to_json(),
+ {
+ 'name': 'Ralph',
+ 'url': 'https://fedoraproject.org/wiki/User:Ralph',
+ }
+ )
+
+ obj = models.Candidate.get(self.session, 2)
+ self.assertEqual(
+ obj.to_json(),
+ {
+ 'name': 'Kevin',
+ 'url': 'https://fedoraproject.org/wiki/User:Kevin',
+ }
+ )
+
+ def test_vote_count(self):
+ """ Test the Candidate.vote_count function. """
+ self.test_init_candidate()
+
+ obj = models.Candidate.by_id(self.session, 3)
+ self.assertEqual(obj.vote_count, 0)
+
+ obj = models.Candidate.get(self.session, 2)
+ self.assertEqual(obj.vote_count, 0)
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(Candidatetests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit 87e90f1909b00b7a44ac05f3bd721c31a9644e40
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:26:34 2014 +0200
Add unit-tests for the Election object
diff --git a/tests/test_election.py b/tests/test_election.py
new file mode 100644
index 0000000..73709d2
--- /dev/null
+++ b/tests/test_election.py
@@ -0,0 +1,248 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections.model.Election test script
+"""
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import unittest
+import sys
+import os
+
+from datetime import time
+from datetime import timedelta
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+from fedora_elections import models
+from tests import Modeltests, TODAY
+
+
+# pylint: disable=R0904
+class Electiontests(Modeltests):
+ """ Election tests. """
+
+ session = None
+
+ def test_init_election(self):
+ """ Test the Election init function. """
+ # Election finished - result open
+ obj = models.Election( # id:1
+ shortdesc='test election shortdesc',
+ alias='test_election',
+ description='test election description',
+ url='https://fedoraproject.org',
+ start_date=TODAY - timedelta(days=10),
+ end_date=TODAY - timedelta(days=8),
+ seats_elected=1,
+ embargoed=0,
+ voting_type='range',
+ candidates_are_fasusers=0,
+ fas_user='ralph',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ # Election finished - result embargoed
+ obj = models.Election( # id:2
+ shortdesc='test election 2 shortdesc',
+ alias='test_election2',
+ description='test election 2 description',
+ url='https://fedoraproject.org',
+ start_date=TODAY - timedelta(days=7),
+ end_date=TODAY - timedelta(days=5),
+ seats_elected=1,
+ embargoed=1,
+ voting_type='range',
+ candidates_are_fasusers=0,
+ fas_user='kevin',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ # Election open
+ obj = models.Election( # id:3
+ shortdesc='test election 3 shortdesc',
+ alias='test_election3',
+ description='test election 3 description',
+ url='https://fedoraproject.org',
+ start_date=TODAY - timedelta(days=2),
+ end_date=TODAY + timedelta(days=2),
+ seats_elected=1,
+ embargoed=1,
+ voting_type='range',
+ candidates_are_fasusers=0,
+ fas_user='pingou',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ # Election created but not open
+ obj = models.Election( # id:4
+ shortdesc='test election 4 shortdesc',
+ alias='test_election4',
+ description='test election 4 description',
+ url='https://fedoraproject.org',
+ start_date=TODAY + timedelta(days=3),
+ end_date=TODAY + timedelta(days=6),
+ seats_elected=1,
+ embargoed=1,
+ voting_type='range',
+ candidates_are_fasusers=0,
+ fas_user='skvidal',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ def test_get_election(self):
+ """ Test the Election.get function. """
+ self.test_init_election()
+ obj = models.Election.get(self.session, 'test_election')
+ self.assertNotEqual(obj, None)
+ self.assertEqual(obj.id, 1)
+
+ def test_status_election(self):
+ """ Test the Election.status function. """
+ self.test_init_election()
+ obj = models.Election.get(self.session, 'test_election')
+ self.assertNotEqual(obj, None)
+ self.assertEqual(obj.status, 'Ended')
+
+ obj = models.Election.get(self.session, 'test_election2')
+ self.assertNotEqual(obj, None)
+ self.assertEqual(obj.status, 'Embargoed')
+
+ obj = models.Election.get(self.session, 'test_election3')
+ self.assertNotEqual(obj, None)
+ self.assertEqual(obj.status, 'In progress')
+
+ obj = models.Election.get(self.session, 'test_election4')
+ self.assertNotEqual(obj, None)
+ self.assertEqual(obj.status, 'Pending')
+
+ def test_locked_election(self):
+ """ Test the Election.locked function. """
+ self.test_init_election()
+ obj = models.Election.get(self.session, 'test_election')
+ self.assertNotEqual(obj, None)
+ self.assertTrue(obj.locked)
+
+ obj = models.Election.get(self.session, 'test_election2')
+ self.assertNotEqual(obj, None)
+ self.assertTrue(obj.locked)
+
+ obj = models.Election.get(self.session, 'test_election3')
+ self.assertNotEqual(obj, None)
+ self.assertTrue(obj.locked)
+
+ obj = models.Election.get(self.session, 'test_election4')
+ self.assertNotEqual(obj, None)
+ self.assertFalse(obj.locked)
+
+ def test_search_election(self):
+ """ Test the Election.search function. """
+ self.test_init_election()
+ obj = models.Election.search(self.session, alias='test_election')
+ self.assertNotEqual(obj, None)
+ self.assertNotEqual(obj, [])
+ self.assertEqual(len(obj), 1)
+ self.assertEqual(obj[0].shortdesc, 'test election shortdesc')
+
+ obj = models.Election.search(
+ self.session, shortdesc='test election 2 shortdesc')
+ self.assertNotEqual(obj, None)
+ self.assertNotEqual(obj, [])
+ self.assertEqual(len(obj), 1)
+ self.assertEqual(obj[0].description, 'test election 2 description')
+
+ obj = models.Election.search(
+ self.session, fas_user='skvidal')
+ self.assertNotEqual(obj, None)
+ self.assertNotEqual(obj, [])
+ self.assertEqual(len(obj), 1)
+ self.assertEqual(obj[0].description, 'test election 4 description')
+
+ obj = models.Election.search(self.session)
+ self.assertNotEqual(obj, None)
+ self.assertNotEqual(obj, [])
+ self.assertEqual(len(obj), 4)
+ self.assertEqual(obj[0].description, 'test election 4 description')
+ self.assertEqual(obj[1].description, 'test election 3 description')
+ self.assertEqual(obj[2].description, 'test election 2 description')
+ self.assertEqual(obj[3].description, 'test election description')
+
+ def test_get_older_election(self):
+ """ Test the Election.get_older_election function. """
+ self.test_init_election()
+ obj = models.Election.get_older_election(self.session, limit=TODAY)
+ self.assertNotEqual(obj, None)
+ self.assertNotEqual(obj, [])
+ self.assertEqual(len(obj), 2)
+ self.assertEqual(obj[0].shortdesc, 'test election 2 shortdesc')
+ self.assertEqual(obj[1].shortdesc, 'test election shortdesc')
+
+ def test_get_open_election(self):
+ """ Test the Election.get_open_election function. """
+ self.test_init_election()
+ obj = models.Election.get_open_election(self.session, limit=TODAY)
+ self.assertNotEqual(obj, None)
+ self.assertNotEqual(obj, [])
+ self.assertEqual(len(obj), 1)
+ self.assertEqual(obj[0].shortdesc, 'test election 3 shortdesc')
+
+ def test_get_next_election(self):
+ """ Test the Election.get_next_election function. """
+ self.test_init_election()
+ obj = models.Election.get_next_election(self.session, limit=TODAY)
+ self.assertNotEqual(obj, None)
+ self.assertNotEqual(obj, [])
+ self.assertEqual(len(obj), 1)
+ self.assertEqual(obj[0].shortdesc, 'test election 4 shortdesc')
+
+ def test_to_json(self):
+ """ Test the Election.to_json function. """
+ self.test_init_election()
+ obj = models.Election.get(self.session, 'test_election')
+ self.assertEqual(
+ obj.to_json(),
+ {
+ 'description': 'test election description',
+ 'end_date': (
+ TODAY - timedelta(days=8)).strftime('%Y-%m-%d %H:%M'),
+ 'url': 'https://fedoraproject.org',
+ 'embargoed': 0,
+ 'alias': 'test_election',
+ 'shortdesc': 'test election shortdesc',
+ 'voting_type': 'range',
+ 'start_date': (
+ TODAY - timedelta(days=10)).strftime('%Y-%m-%d %H:%M'),
+ }
+ )
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(Electiontests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit 370d447f811cf3f51ed23e046f488dcc1d84b310
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:25:29 2014 +0200
Add first code for the unit-tests
Create the basic `Modeltests` class that all the tests will inherit from
Create basic functions to be used when testing the flask application
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..59e94fe
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,145 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections test script
+"""
+
+__requires__ = ['SQLAlchemy >= 0.7']
+import pkg_resources
+
+import unittest
+import sys
+import os
+
+from datetime import date
+from datetime import timedelta
+from functools import wraps
+
+from contextlib import contextmanager
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import scoped_session
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+from fedora_elections import models
+
+
+DB_PATH = 'sqlite:///:memory:'
+FAITOUT_URL = 'http://209.132.184.152/faitout/'
+try:
+ import requests
+ req = requests.get('%s/new' % FAITOUT_URL)
+ if req.status_code == 200:
+ DB_PATH = req.text
+ print 'Using faitout at: %s' % DB_PATH
+except:
+ pass
+
+
+TODAY = date.today()
+
+
+@contextmanager
+def user_set(APP, user):
+ """ Set the provided user as fas_user in the provided application."""
+
+ # Hack used to remove the before_request function set by
+ # flask.ext.fas_openid.FAS which otherwise kills our effort to set a
+ # flask.g.fas_user.
+ from flask import appcontext_pushed, g
+ APP.before_request_funcs[None] = []
+
+ def handler(sender, **kwargs):
+ g.fas_user = user
+
+ with appcontext_pushed.connected_to(handler, APP):
+ yield
+
+
+class Modeltests(unittest.TestCase):
+ """ Model tests. """
+
+ def __init__(self, method_name='runTest'):
+ """ Constructor. """
+ unittest.TestCase.__init__(self, method_name)
+ self.session = None
+
+ # pylint: disable=C0103
+ def setUp(self):
+ """ Set up the environnment, ran before every tests. """
+ self.session = models.create_tables(DB_PATH)
+
+ # pylint: disable=C0103
+ def tearDown(self):
+ """ Remove the test.db database if there is one. """
+ self.session.close()
+ if os.path.exists(DB_PATH):
+ os.unlink(DB_PATH)
+ if DB_PATH.startswith('postgres'):
+ if 'localhost' in DB_PATH:
+ models.drop_tables(DB_PATH, self.session.bind)
+ else:
+ db_name = DB_PATH.rsplit('/', 1)[1]
+ requests.get('%s/clean/%s' % (FAITOUT_URL, db_name))
+
+
+class FakeGroup(object):
+ """ Fake object used to make the FakeUser object closer to the
+ expectations.
+ """
+
+ def __init__(self, name):
+ """ Constructor.
+ :arg name: the name given to the name attribute of this object.
+ """
+ self.name = name
+ self.group_type = 'cla'
+
+
+# pylint: disable=R0903
+class FakeUser(object):
+ """ Fake user used to test the fedocallib library. """
+
+ def __init__(self, groups=[], username='username', cla_done=True):
+ """ Constructor.
+ :arg groups: list of the groups in which this fake user is
+ supposed to be.
+ """
+ if isinstance(groups, basestring):
+ groups = [groups]
+ self.groups = groups
+ self.username = username
+ self.name = username
+ self.approved_memberships = [
+ FakeGroup('packager'),
+ FakeGroup('design-team')]
+ self.dic = {}
+ self.dic['timezone'] = 'Europe/Paris'
+ self.cla_done = cla_done
+
+ def __getitem__(self, key):
+ return self.dic[key]
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(Modeltests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit 0d05ea00928e73fb60ffe86b31d7020d26eba358
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:24:58 2014 +0200
Ignore a small piece of the createdb function in the unit-tests
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 579d4de..8eb511e 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -40,7 +40,7 @@ def create_tables(db_url, alembic_ini=None, debug=False):
engine = create_engine(db_url, echo=debug)
BASE.metadata.create_all(engine)
#engine.execute(collection_package_create_view(driver=engine.driver))
- if db_url.startswith('sqlite:'):
+ if db_url.startswith('sqlite:'): # pragma: no cover
## Ignore the warning about con_record
# pylint: disable=W0613
def _fk_pragma_on_connect(dbapi_con, con_record):
commit 2944a4603842136eb872e31169e50022fccb720a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:22:11 2014 +0200
Order votes by timestamp
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index e0ba52b..579d4de 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -302,6 +302,8 @@ class Vote(BASE):
cls.election_id == election_id
).filter(
cls.voter == user
+ ).order_by(
+ cls.timestamp
)
if count:
commit e007c5da1c7099d618469c01729b073db9f16e60
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:12:17 2014 +0200
Format the start and end dates in the json representation of an election
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 9e3c2ae..e0ba52b 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -105,8 +105,8 @@ class Election(BASE):
alias=self.alias,
description=self.description,
url=self.url,
- start_date=self.start_date,
- end_date=self.end_date,
+ start_date=self.start_date.strftime('%Y-%m-%d %H:%M'),
+ end_date=self.end_date.strftime('%Y-%m-%d %H:%M'),
embargoed=self.embargoed,
voting_type=self.voting_type,
)
commit dbf3e2d28a40d4135a24e1c60650b043265cc5c8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:09:33 2014 +0200
Add default datetime in the timestamp field of the Vote table
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 90054bf..9e3c2ae 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -3,6 +3,7 @@
import sqlalchemy as sa
from sqlalchemy import create_engine
+from sqlalchemy import func as safunc
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.ext.declarative import declarative_base
@@ -282,7 +283,7 @@ class Vote(BASE):
election = relationship(
'Election', backref=backref('votes', lazy='dynamic'))
voter = sa.Column(sa.Unicode(150), nullable=False)
- timestamp = sa.Column(sa.DateTime, nullable=False)
+ timestamp = sa.Column(sa.DateTime, nullable=False, default=safunc.now())
candidate_id = sa.Column(sa.Integer, sa.ForeignKey('candidates.id'),
nullable=False)
candidate = relationship(
commit 50c91f1fb2a4a6dacda9f93419642f5e6b669b73
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:08:34 2014 +0200
pep8 fixes on the controllers
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index a11898d..9873635 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -67,7 +67,7 @@ from fedora_elections import forms
def is_authenticated():
''' Return a boolean specifying if the user is authenticated or not.
'''
- return hasattr(flask.g, 'fas_user') and not flask.g.fas_user is None
+ return hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None
def is_safe_url(target):
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index d0a6f43..a943086 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -33,9 +33,10 @@ from sqlalchemy.orm.exc import NoResultFound
from fedora_elections import fedmsgshim
from fedora_elections import forms
from fedora_elections import models
-from fedora_elections import APP, SESSION, is_authenticated, is_admin, \
- is_election_admin, is_safe_url, \
- safe_redirect_back
+from fedora_elections import (
+ APP, SESSION, is_authenticated, is_admin, is_election_admin,
+ is_safe_url, safe_redirect_back
+)
def election_admin_required(f):
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index b556e13..0afb02e 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -33,8 +33,10 @@ from sqlalchemy.orm.exc import NoResultFound
from fedora_elections import fedmsgshim
from fedora_elections import forms
from fedora_elections import models
-from fedora_elections import APP, SESSION, is_authenticated, is_admin,\
- is_safe_url, safe_redirect_back
+from fedora_elections import (
+ APP, SESSION, is_authenticated, is_admin, is_safe_url,
+ safe_redirect_back,
+)
def login_required(f):
@@ -76,7 +78,6 @@ def get_valid_election(election_alias, ended=False):
return election
-
@APP.route('/vote/<election_alias>', methods=['GET', 'POST'])
@login_required
def vote(election_alias):
commit 6b7e934c87169401b1fd949507139e6b8bfe20c6
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 11:08:34 2014 +0200
Make the minimal flask version be 0.10
diff --git a/requirements.txt b/requirements.txt
index ca5b050..28de0aa 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
# Used for when working from a virtualenv.
# Use this file by running "$ pip install -r requirements.txt"
-flask
+flask>=0.10
kitchen
python-fedora
Flask-wtf
commit 17dd53ea5b2e7566a2482ae8142179373a37917d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 11:05:52 2014 +0200
Remove current tests
diff --git a/tests/base.py b/tests/base.py
deleted file mode 100644
index c0e5c73..0000000
--- a/tests/base.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2012-2014 Red Hat, Inc.
-#
-# Author(s): Toshio Kuratomi <toshio(a)fedoraproject.org>
-
-import os
-
-import fedora_elections
-from fedora_elections.models import create_tables
-
-# Note: This is used in unittests that use nose's generator tests so we can't
-# subclass unittest.TestCase
-class BaseRequestTests(object):
- '''Setup application for further testing of requests'''
-
- def setUp(self):
- cur_folder = os.path.dirname(os.path.realpath(__file__))
- db_file = os.path.join(cur_folder, 'testing.sqlite')
- print '* creating %s' % db_file
- fedora_elections.APP.config['DB_URL'] = 'sqlite:///%s' % db_file
-
- self.session = create_tables(fedora_elections.APP.config['DB_URL'])
-
- fedora_elections.APP.config['TESTING'] = True
- fedora_elections.APP.SESSION = self.session
- self.app = fedora_elections.APP.test_client()
-
- def tearDown(self):
- cur_folder = os.path.dirname(os.path.realpath(__file__))
- db_file = os.path.join(cur_folder, 'testing.sqlite')
- print '* cleaning %s' % db_file
- os.unlink(db_file)
-
-
-url_subs = {'election_alias': None, 'candidate_id': None}
-
-anon_urls = (
- '/',
- '/archive',
- '/login',
- '/logout',
- '/vote/%(election_alias)s')
-
-authed_view_urls = (
- '/logout',
- '/vote/%(election_alias)s')
-
-admin_view_urls = (
- '/admin',
- '/admin/new',
- '/admin/%(election_alias)s')
-
-admin_modify_urls = (
- '/admin/new',
- '/admin/%(election_alias)s/edit',
- '/admin/%(election_alias)s/candidates/new',
- '/admin/%(election_alias)s/candidates/%(candidate_id)d/edit',
- '/admin/%(election_alias)s/candidates/%(candidate_id)d/delete')
diff --git a/tests/test_smoke.py b/tests/test_smoke.py
deleted file mode 100644
index 0fbb3d2..0000000
--- a/tests/test_smoke.py
+++ /dev/null
@@ -1,100 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2012 Red Hat, Inc.
-#
-# Author(s): Toshio Kuratomi <toshio(a)fedoraproject.org>
-
-from copy import copy
-import datetime
-from functools import partial
-
-from mock import Mock
-from nose import tools, with_setup
-
-from base import BaseRequestTests, anon_urls, authed_view_urls, \
- admin_view_urls, admin_modify_urls, url_subs
-
-import fedora_elections
-import fedora_elections.models
-
-class TestSmoke(BaseRequestTests):
- '''Basic test that we don't get 500s'''
-
- def setUp(self):
- super(TestSmoke, self).setUp()
- fedora_elections.SESSION = self.session
-
- fedora_elections.FAS.login = Mock(return_value=True)
- # Add an election
- election = fedora_elections.models.Election(
- summary='Test Election 1',
- alias='test1',
- description='lorem ipsum of course',
- url='http://fedorahosted.org/elections',
- start_date=datetime.datetime.utcnow(),
- end_date=datetime.datetime.utcnow()+datetime.timedelta(hours=1),
- fas_user='fchiulli',
- )
- self.session.add(election)
-
- # Add candidate to election
- candidate = fedora_elections.models.Candidate(
- election=election,
- name='Mr Testy',
- url='http://fedoraproject.org')
- self.session.add(candidate)
-
- self.session.commit()
-
- self.url_subs = copy(url_subs)
- self.url_subs['election_alias'] = 'test1'
- self.url_subs['candidate_id'] = candidate.id
-
- def tearDown(self):
- super(TestSmoke, self).tearDown()
-
- def setup_auth(self, username, password):
- self.setUp()
- self.app.post('/login', data=dict( username=username,
- password=password), follow_redirects=True)
-
- def teardown_auth(self):
- self.tearDown()
- return self.app.get('/logout', follow_redirects=True)
-
- def check_status_code(self, url, status_codes=[200]):
- resp = self.app.get(url % self.url_subs, follow_redirects=True)
- tools.assert_in(resp.status_code, status_codes)
-
- @with_setup(partial(setup_auth, 'user', '123'), teardown_auth)
- def check_user_status_code(self, *args, **kwargs):
- self.check_status_code(*args, **kwargs)
-
- @with_setup(partial(setup_auth, 'admin', '123'), teardown_auth)
- def check_admin_status_code(self, *args, **kwargs):
- self.check_status_code(*args, **kwargs)
-
- def test_all_url_subs(self):
- '''Testing that the testsuite is going to substitute all variables
- in the url
- '''
- for i in self.url_subs.values():
- if i is None:
- raise Exception('Smoke test must be updated for new url '
- 'variable present in base.py')
-
- def test_anon_view(self):
- for url in anon_urls:
- yield self.check_status_code, url, [200]
-
- def test_authed_view(self):
- for url in (u for u in authed_view_urls):
- yield self.check_user_status_code, url, [200]
-
- def test_admin_view(self):
- for url in (u for u in admin_view_urls):
- yield self.check_admin_status_code, url, [200]
-
- def test_admin_modify(self):
- for url in (u for u in admin_modify_urls):
- yield self.check_admin_status_code, url, [200]
commit bd2db38ad9e04f4bd1a26e833b6e88f8a4a12ff6
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 14:11:06 2014 +0200
Adjust the database mapping of candidates_are_fasusers to its type in the DB
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 4c4e300..90054bf 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -93,8 +93,8 @@ class Election(BASE):
seats_elected = sa.Column(sa.Integer, nullable=False, default=1)
embargoed = sa.Column(sa.Integer, nullable=False, default=0)
voting_type = sa.Column(sa.Unicode(100), nullable=False, default=u'range')
- candidates_are_fasusers = sa.Column(sa.Boolean, nullable=False,
- default=False)
+ candidates_are_fasusers = sa.Column(
+ sa.Integer, nullable=False, default=0)
fas_user = sa.Column(sa.Unicode(50), nullable=False)
def to_json(self):
commit 599405957ed21ed522b2f77b51807aa1f975f630
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 14:05:09 2014 +0200
Rename the column usefas to candidates_are_fasusers
diff --git a/files/update_1_to_2.sql b/files/update_1_to_2.sql
index 99da757..79bb69c 100644
--- a/files/update_1_to_2.sql
+++ b/files/update_1_to_2.sql
@@ -5,7 +5,7 @@
ALTER TABLE elections ADD COLUMN voting_type VARCHAR(100) NOT NULL DEFAULT 'range';
-ALTER TABLE elections ADD COLUMN candidates_are_fasusers BOOLEAN NOT NULL DEFAULT false;
+-- ALTER TABLE elections ADD COLUMN candidates_are_fasusers BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE elections ADD COLUMN fas_user VARCHAR(50);
@@ -15,3 +15,7 @@ ALTER TABLE elections DROP COLUMN votes_per_user;
-- ALTER TABLE elections RENAME COLUMN shortdesc TO summary;
+
+ALTER TABLE elections DROP COLUMN candidates_are_fasusers;
+
+ALTER TABLE elections RENAME COLUMN usefas TO candidates_are_fasusers;
commit 3054b9dcad3db5e936c7b3971f800279070fd1c0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:21:10 2014 +0200
Remove import of the redirect file since the file no longer exists
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index dfa5e09..a11898d 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -62,7 +62,6 @@ FAS2 = AccountSystem(
from fedora_elections import models
SESSION = models.create_session(APP.config['DB_URL'])
from fedora_elections import forms
-from fedora_elections import redirect
def is_authenticated():
commit 316505cfddfcd5742d5ca03d454471f2b25060eb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 11:04:14 2014 +0200
Make the runserver.py file executable
diff --git a/runserver.py b/runserver.py
old mode 100644
new mode 100755
commit 54f977529c0c24df4b93f3c16ae9049dd3fb4bc8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 10:47:35 2014 +0200
Give a warning information message to admins seeing an embargoed election
And format a little the warning messages (as orange)
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index b12c6c7..b556e13 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -286,9 +286,11 @@ def election_results(election_alias):
else:
if is_admin(flask.g.fas_user) \
or is_election_admin(flask.g.fas_user, election.id):
+ flask.flash("You are only seeing this page because you are "
+ "an admin.", "warning")
flask.flash("The results for this election are currently "
"embargoed pending formal announcement.",
- "error")
+ "warning")
pass
else:
flask.flash(
diff --git a/fedora_elections/static/elections.css b/fedora_elections/static/elections.css
index 5322b11..8aba889 100644
--- a/fedora_elections/static/elections.css
+++ b/fedora_elections/static/elections.css
@@ -7,6 +7,10 @@
}
.warning {
+ color: orange;
+}
+
+.warning {
color: #FF8000;
}
commit 0d808399e3ca1717d8ea587ecef29bc0bf2330eb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 10:34:38 2014 +0200
Split the one controller into three
- __init__ has the main endpoints where no login is necessary
- elections has the elections related endpoints (voting, results...)
- admin has the administration related endpoints (create, edit, delete...)
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index cfe4695..dfa5e09 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -117,55 +117,24 @@ def is_election_admin(user, election_id):
return len(set(user.groups).intersection(set(admingroups))) > 0
-def login_required(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- if not is_authenticated():
- return flask.redirect(flask.url_for(
- 'auth_login', next=flask.request.url))
- return f(*args, **kwargs)
- return decorated_function
-
-
-def election_admin_required(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- if not is_authenticated():
- return flask.redirect(flask.url_for(
- 'auth_login', next=flask.request.url))
- if not is_admin(flask.g.fas_user):
- flask.abort(403)
- return f(*args, **kwargs)
- return decorated_function
-
-
-def get_valid_election(election_alias, ended=False):
- """ Return the election if it is valid (no pending, not ended).
- """
- election = models.Election.get(SESSION, alias=election_alias)
-
- if not election:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
-
- if election.status == 'Pending':
- flask.flash('Voting has not yet started, sorry.')
- return redirect.safe_redirect_back()
-
- elif not ended and election.status == 'Ended':
- flask.flash(
- 'This election is closed. You have been redirected to the '
- 'election results.')
- return flask.redirect(flask.url_for(
- 'election_results', election_alias=election.alias))
+def is_safe_url(target):
+ ref_url = urlparse(flask.request.host_url)
+ test_url = urlparse(urljoin(flask.request.host_url, target))
+ return test_url.scheme in ('http', 'https') and \
+ ref_url.netloc == test_url.netloc
- elif ended and election.status == 'In progress':
- flask.flash(
- 'Sorry but this election is in progress, you may not see its '
- 'results already.')
- return redirect.safe_redirect_back()
- return election
+def safe_redirect_back(next=None, fallback=('index', {})):
+ targets = []
+ if next:
+ targets.append(next)
+ if 'next' in flask.request.args and \
+ flask.request.args['next']:
+ targets.append(flask.request.args['next'])
+ targets.append(flask.url_for(fallback[0], **fallback[1]))
+ for target in targets:
+ if is_safe_url(target):
+ return flask.redirect(target)
@APP.context_processor
@@ -214,7 +183,7 @@ def about_election(election_alias):
if not election:
flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
+ return safe_redirect_back()
usernamemap = {}
if (election.candidates_are_fasusers):
@@ -240,7 +209,7 @@ def archived_elections():
if not elections:
flask.flash('There are no archived elections.')
- return redirect.safe_redirect_back()
+ return safe_redirect_back()
return flask.render_template(
'archive.html',
@@ -255,7 +224,7 @@ def open_elections():
if not elections:
flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
+ return safe_redirect_back()
voted = []
if is_authenticated():
@@ -273,207 +242,10 @@ def open_elections():
title='Open Elections')
-### ELECTION VIEWS #############################################
-
-(a)APP.route('/vote/<election_alias>', methods=['GET', 'POST'])
-@login_required
-def vote(election_alias):
- election = get_valid_election(election_alias)
-
- if not isinstance(election, models.Election):
- return election
-
- votes = models.Vote.of_user_on_election(
- SESSION, flask.g.fas_user.username, election.id, count=True)
-
- if votes > 0:
- flask.flash('You have already voted in the election!')
- return redirect.safe_redirect_back()
-
- if election.voting_type == 'range':
- return vote_range(election_alias)
- elif election.voting_type == 'simple':
- return vote_simple(election_alias)
- else:
- flask.flash(
- 'Unknown election voting type: %s' % election.voting_type)
- return redirect.safe_redirect_back()
-
-
-(a)APP.route('/vote_range/<election_alias>', methods=['GET', 'POST'])
-@login_required
-def vote_range(election_alias):
- election = get_valid_election(election_alias)
-
- if not isinstance(election, models.Election):
- return election
-
- if (election.voting_type == 'simple'):
- return flask.redirect(flask.url_for(
- 'vote_simple', election_alias=election_alias))
-
- votes = models.Vote.of_user_on_election(
- SESSION, flask.g.fas_user.username, election.id, count=True)
-
- if votes > 0:
- flask.flash('You have already voted in the election!')
- return redirect.safe_redirect_back()
-
- num_candidates = election.candidates.count()
-
- uvotes = {}
- for candidate in election.candidates:
- uvotes[candidate.id] = ""
- next_action = 'vote'
-
- if flask.request.method == 'POST':
- next_action = ''
- form_values = flask.request.form.values()
- if 'Submit' in form_values:
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote >= 0 and vote <= num_candidates:
- uvotes[candidate.id] = int(vote)
- else:
- flask.flash("Invalid Ballot!")
- return redirect.safe_redirect_back()
-
- except ValueError:
- flask.flash("Invalid Ballot!")
- return redirect.safe_redirect_back()
-
- for uvote in uvotes:
- new_vote = models.Vote(
- election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=uvote,
- value=uvotes[uvote]
- )
- SESSION.add(new_vote)
- SESSION.commit()
-
- flask.flash("Your vote has been recorded. Thank you!")
- return redirect.safe_redirect_back()
-
- elif 'Preview' in form_values:
- flask.flash("Please confirm your vote!")
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote > num_candidates:
- flask.flash("Invalid data.")
- uvotes[candidate.id] = 0
- next_action = 'vote'
- elif vote >= 0:
- uvotes[candidate.id] = vote
- else:
- flask.flash("Invalid data2.")
- uvotes[c.id] = 0
- next_action = 'vote'
- except ValueError:
- flask.flash("Invalid data")
-
- if (next_action != 'vote'):
- next_action = 'confirm'
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- FAS2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template(
- 'vote_range.html',
- election=election,
- num_candidates=num_candidates,
- usernamemap=usernamemap,
- voteinfo=uvotes,
- nextaction=next_action)
-
-
-(a)APP.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
-@login_required
-def vote_simple(election_alias):
- election = get_valid_election(election_alias)
-
- if not isinstance(election, models.Election):
- return election
-
- if (election.voting_type == 'range'):
- return flask.redirect(flask.url_for(
- 'vote_range', election_alias=election_alias))
-
- votes = models.Vote.of_user_on_election(
- SESSION, flask.g.fas_user.username, election.id, count=True)
-
- if (votes != 0):
- flask.flash('You have already voted in the election!')
- return redirect.safe_redirect_back()
-
- num_candidates = election.candidates.count()
- next_action = 'vote'
- candidate_id = -1
-
- if flask.request.method == 'POST':
- next_action = ''
- form_values = flask.request.form.values()
- if 'Submit' in form_values:
- candidate_id = int(flask.request.form['candidate'])
- new_vote = models.Vote(
- election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=candidate_id,
- value=1
- )
- SESSION.add(new_vote)
- SESSION.commit()
-
- flask.flash("Your vote has been recorded. Thank you!")
- return redirect.safe_redirect_back()
-
- elif 'Preview' in form_values:
- if ('candidate' in flask.request.form):
- flask.flash("Please confirm your vote!")
- candidate_id = int(flask.request.form['candidate'])
- else:
- flask.flash("Please vote for a candidate")
- next_action = 'vote'
-
- if (next_action != 'vote'):
- next_action = 'confirm'
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- FAS2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template(
- 'vote_simple.html',
- election=election,
- num_candidates=num_candidates,
- usernamemap=usernamemap,
- candidate_id=candidate_id,
- nextaction=next_action)
-
-
-### AUTH VIEWS #############################################
-
@APP.route('/login', methods=('GET', 'POST'))
def auth_login():
if flask.g.fas_user:
- return redirect.safe_redirect_back()
+ return safe_redirect_back()
next_url = None
if 'next' in flask.request.args:
if is_safe_url(flask.request.args['next']):
@@ -494,311 +266,9 @@ def auth_logout():
FAS.logout()
flask.g.fas_user = None
flask.flash('You have been logged out')
- return redirect.safe_redirect_back()
-
-
-### ELECTION ADMIN VIEWS ###################################
+ return safe_redirect_back()
-(a)APP.route('/admin/')
-@election_admin_required
-def admin_view_elections():
- elections = models.Election.search(SESSION)
-
- return flask.render_template(
- 'admin/all_elections.html',
- elections=elections)
-
-(a)APP.route('/admin/new', methods=('GET', 'POST'))
-@election_admin_required
-def admin_new_election():
- form = forms.ElectionForm()
- if form.validate_on_submit():
-
- election = models.Election(
- shortdesc=form.shortdesc.data,
- alias=form.alias.data,
- description=form.description.data,
- url=form.url.data,
- start_date=form.start_date.data,
- end_date=form.end_date.data,
- seats_elected=form.seats_elected.data,
- embargoed=int(form.embargoed.data),
- voting_type=form.voting_type.data,
- candidates_are_fasusers=form.candidates_are_fasusers.data,
- fas_user=flask.g.fas_user.username,
- )
-
- # Fix start_date and end_date to use datetime
- election.start_date = datetime.combine(election.start_date, time())
- election.end_date = datetime.combine(election.end_date,
- time(23, 59, 59))
- SESSION.add(election)
-
- admin = models.ElectionAdminGroup(
- election=election,
- group_name=APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
- )
- SESSION.add(admin)
-
- SESSION.commit()
-
- fedmsgshim.publish(
- topic="election.new",
- msg=dict(
- agent=flask.g.fas_user.username,
- election=election.to_json(),
- )
- )
- flask.flash('Election "%s" added' % election.alias)
- return flask.redirect(flask.url_for(
- 'admin_view_election', election_alias=election.alias))
- return flask.render_template(
- 'admin/election_form.html',
- form=form,
- submit_text='Create election')
-
-
-(a)APP.route('/admin/<election_alias>/')
-@election_admin_required
-def admin_view_election(election_alias):
- election = models.Election.get(SESSION, alias=election_alias)
- if not election:
- flask.abort(404)
-
- return flask.render_template(
- 'admin/view_election.html',
- election=election)
-
-
-(a)APP.route('/admin/<election_alias>/edit', methods=('GET', 'POST'))
-@election_admin_required
-def admin_edit_election(election_alias):
- election = models.Election.get(SESSION, alias=election_alias)
- if not election:
- flask.abort(404)
-
- form = forms.ElectionForm(election.id, obj=election)
- if form.validate_on_submit():
- form.embargoed.data = int(form.embargoed.data)
- form.populate_obj(election)
- SESSION.commit()
- fedmsgshim.publish(
- topic="election.edit",
- msg=dict(
- agent=flask.g.fas_user.username,
- election=election.to_json(),
- )
- )
- flask.flash('Election "%s" saved' % election.alias)
- return flask.redirect(flask.url_for(
- 'admin_view_election', election_alias=election.alias))
-
- return flask.render_template(
- 'admin/election_form.html',
- form=form,
- submit_text='Edit election')
-
-
-(a)APP.route('/admin/<election_alias>/candidates/new', methods=('GET', 'POST'))
-@election_admin_required
-def admin_add_candidate(election_alias):
- election = models.Election.get(SESSION, alias=election_alias)
- if not election:
- flask.abort(404)
-
- form = forms.CandidateForm()
- if form.validate_on_submit():
- candidate = models.Candidate(
- election=election,
- name=form.name.data,
- url=form.url.data
- )
-
- SESSION.add(candidate)
- SESSION.commit()
- flask.flash('Candidate "%s" saved' % candidate.name)
- fedmsgshim.publish(
- topic="candidate.new",
- msg=dict(
- agent=flask.g.fas_user.username,
- election=candidate.election.to_json(),
- candidate=candidate.to_json(),
- )
- )
- return flask.redirect(flask.url_for(
- 'admin_view_election', election_alias=election.alias))
-
- return flask.render_template(
- 'admin/candidate.html',
- form=form,
- submit_text='Add candidate')
-
-
-(a)APP.route('/admin/<election_alias>/candidates/new/multi',
- methods=('GET', 'POST'))
-@election_admin_required
-def admin_add_multi_candidate(election_alias):
- election = models.Election.get(SESSION, alias=election_alias)
- if not election:
- flask.abort(404)
-
- form = forms.MultiCandidateForm()
- if form.validate_on_submit():
-
- candidates_name = []
- for entry in form.candidate.data.strip().split("|"):
- candidate = entry.split("!")
- #No url
- if len(candidate) == 1:
- cand = models.Candidate(
- election=election,
- name=candidate[0])
- SESSION.add(cand)
- candidates_name.append(cand.name)
- # With url
- elif len(candidate) == 2:
- cand = models.Candidate(
- election=election,
- name=candidate[0],
- url=candidate[1])
- SESSION.add(cand)
- candidates_name.append(cand.name)
- else:
- flask.flash("There was an issue!")
- fedmsgshim.publish(
- topic="candidate.new",
- msg=dict(
- agent=flask.g.fas_user.username,
- election=cand.election.to_json(),
- candidate=cand.to_json(),
- )
- )
-
- SESSION.commit()
- flask.flash('Added %s candidates' % len(candidates_name))
- return flask.redirect(flask.url_for(
- 'admin_view_election', election_alias=election.alias))
-
- return flask.render_template(
- 'admin/candidate_multi.html',
- form=form,
- submit_text='Add candidates')
-
-
-(a)APP.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
- methods=('GET', 'POST'))
-@election_admin_required
-def admin_edit_candidate(election_alias, candidate_id):
- election = models.Election.get(SESSION, alias=election_alias)
- candidate = models.Candidate.get(SESSION, candidate_id=candidate_id)
-
- if not election or not candidate:
- flask.abort(404)
-
- if candidate.election != election:
- flask.abort(404)
-
- form = forms.CandidateForm(obj=candidate)
- if form.validate_on_submit():
- form.populate_obj(candidate)
- SESSION.commit()
- flask.flash('Candidate "%s" saved' % candidate.name)
- fedmsgshim.publish(
- topic="candidate.edit",
- msg=dict(
- agent=flask.g.fas_user.username,
- election=candidate.election.to_json(),
- candidate=candidate.to_json(),
- )
- )
- return flask.redirect(flask.url_for(
- 'admin_view_election', election_alias=election.alias))
-
- return flask.render_template(
- 'admin/candidate.html',
- form=form,
- submit_text='Edit candidate')
-
-
-(a)APP.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
- methods=('GET', 'POST'))
-@election_admin_required
-def admin_delete_candidate(election_alias, candidate_id):
- election = models.Election.get(SESSION, alias=election_alias)
- candidate = models.Candidate.get(SESSION, candidate_id=candidate_id)
-
- if not election or not candidate:
- flask.abort(404)
-
- if candidate.election != election:
- flask.abort(404)
-
- form = forms.ConfirmationForm()
- if form.validate_on_submit():
- candidate_name = candidate.name
- SESSION.delete(candidate)
- SESSION.commit()
- flask.flash('Candidate "%s" deleted' % candidate_name)
- fedmsgshim.publish(
- topic="candidate.delete",
- msg=dict(
- agent=flask.g.fas_user.username,
- election=candidate.election.to_json(),
- candidate=candidate.to_json(),
- )
- )
- return flask.redirect(flask.url_for(
- 'admin_view_election', election_alias=election.alias))
-
- return flask.render_template(
- 'admin/delete_candidate.html',
- form=form,
- candidate=candidate)
-
-
-(a)APP.route('/results/<election_alias>')
-def election_results(election_alias):
- election = get_valid_election(election_alias, ended=True)
-
- if not isinstance(election, models.Election):
- return election
-
- elif election.embargoed:
- if not hasattr(flask.g, 'fas_user') or not flask.g.fas_user:
- flask.flash("We are sorry. The results for this election"
- "cannot be viewed because they are currently "
- "embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
- else:
- if is_admin(flask.g.fas_user) \
- or is_election_admin(flask.g.fas_user, election.id):
- flask.flash("The results for this election are currently "
- "embargoed pending formal announcement.",
- "error")
- pass
- else:
- flask.flash(
- "We are sorry. The results for this election"
- "cannot be viewed because they are currently "
- "embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- FAS2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- stats = models.Vote.get_election_stats(SESSION, election.id)
-
- return flask.render_template(
- 'results.html',
- election=election,
- usernamemap=usernamemap,
- stats=stats,
- )
+# Finalize the import of other controllers
+import admin
+import elections
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
new file mode 100644
index 0000000..d0a6f43
--- /dev/null
+++ b/fedora_elections/admin.py
@@ -0,0 +1,310 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2012 Red Hat, Inc.
+# Copyright © 2012 Ian Weller <ianweller(a)fedoraproject.org>
+# Copyright © 2012 Toshio Kuratomi <tkuratom(a)redhat.com>
+# Copyright © 2012 Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+#
+# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
+# Toshio Kuratomi <tkuratom(a)redhat.com>
+# Frank Chiulli <fchiulli(a)fedoraproject.org>
+# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
+#
+
+from datetime import datetime, time
+from functools import wraps
+
+import flask
+from sqlalchemy.orm.exc import NoResultFound
+
+from fedora_elections import fedmsgshim
+from fedora_elections import forms
+from fedora_elections import models
+from fedora_elections import APP, SESSION, is_authenticated, is_admin, \
+ is_election_admin, is_safe_url, \
+ safe_redirect_back
+
+
+def election_admin_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if not is_authenticated():
+ return flask.redirect(flask.url_for(
+ 'auth_login', next=flask.request.url))
+ if not is_admin(flask.g.fas_user):
+ flask.abort(403)
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+(a)APP.route('/admin/')
+@election_admin_required
+def admin_view_elections():
+ elections = models.Election.search(SESSION)
+
+ return flask.render_template(
+ 'admin/all_elections.html',
+ elections=elections)
+
+
+(a)APP.route('/admin/new', methods=('GET', 'POST'))
+@election_admin_required
+def admin_new_election():
+ form = forms.ElectionForm()
+ if form.validate_on_submit():
+
+ election = models.Election(
+ shortdesc=form.shortdesc.data,
+ alias=form.alias.data,
+ description=form.description.data,
+ url=form.url.data,
+ start_date=form.start_date.data,
+ end_date=form.end_date.data,
+ seats_elected=form.seats_elected.data,
+ embargoed=int(form.embargoed.data),
+ voting_type=form.voting_type.data,
+ candidates_are_fasusers=form.candidates_are_fasusers.data,
+ fas_user=flask.g.fas_user.username,
+ )
+
+ # Fix start_date and end_date to use datetime
+ election.start_date = datetime.combine(election.start_date, time())
+ election.end_date = datetime.combine(election.end_date,
+ time(23, 59, 59))
+ SESSION.add(election)
+
+ admin = models.ElectionAdminGroup(
+ election=election,
+ group_name=APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ )
+ SESSION.add(admin)
+
+ SESSION.commit()
+
+ fedmsgshim.publish(
+ topic="election.new",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=election.to_json(),
+ )
+ )
+
+ flask.flash('Election "%s" added' % election.alias)
+ fedmsgshim.publish(topic="election.new", msg=election)
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+ return flask.render_template(
+ 'admin/election_form.html',
+ form=form,
+ submit_text='Create election')
+
+
+(a)APP.route('/admin/<election_alias>/')
+@election_admin_required
+def admin_view_election(election_alias):
+ election = models.Election.get(SESSION, alias=election_alias)
+ if not election:
+ flask.abort(404)
+
+ return flask.render_template(
+ 'admin/view_election.html',
+ election=election)
+
+
+(a)APP.route('/admin/<election_alias>/edit', methods=('GET', 'POST'))
+@election_admin_required
+def admin_edit_election(election_alias):
+ election = models.Election.get(SESSION, alias=election_alias)
+ if not election:
+ flask.abort(404)
+
+ form = forms.ElectionForm(election.id, obj=election)
+ if form.validate_on_submit():
+ form.embargoed.data = int(form.embargoed.data)
+ form.populate_obj(election)
+ SESSION.commit()
+ fedmsgshim.publish(
+ topic="election.edit",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=election.to_json(),
+ )
+ )
+ flask.flash('Election "%s" saved' % election.alias)
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+
+ return flask.render_template(
+ 'admin/election_form.html',
+ form=form,
+ submit_text='Edit election')
+
+
+(a)APP.route('/admin/<election_alias>/candidates/new', methods=('GET', 'POST'))
+@election_admin_required
+def admin_add_candidate(election_alias):
+ election = models.Election.get(SESSION, alias=election_alias)
+ if not election:
+ flask.abort(404)
+
+ form = forms.CandidateForm()
+ if form.validate_on_submit():
+ candidate = models.Candidate(
+ election=election,
+ name=form.name.data,
+ url=form.url.data
+ )
+
+ SESSION.add(candidate)
+ SESSION.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ fedmsgshim.publish(
+ topic="candidate.new",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=candidate.election.to_json(),
+ candidate=candidate.to_json(),
+ )
+ )
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+
+ return flask.render_template(
+ 'admin/candidate.html',
+ form=form,
+ submit_text='Add candidate')
+
+
+(a)APP.route('/admin/<election_alias>/candidates/new/multi',
+ methods=('GET', 'POST'))
+@election_admin_required
+def admin_add_multi_candidate(election_alias):
+ election = models.Election.get(SESSION, alias=election_alias)
+ if not election:
+ flask.abort(404)
+
+ form = forms.MultiCandidateForm()
+ if form.validate_on_submit():
+
+ candidates_name = []
+ for entry in form.candidate.data.strip().split("|"):
+ candidate = entry.split("!")
+ #No url
+ if len(candidate) == 1:
+ cand = models.Candidate(
+ election=election,
+ name=candidate[0])
+ SESSION.add(cand)
+ candidates_name.append(cand.name)
+ # With url
+ elif len(candidate) == 2:
+ cand = models.Candidate(
+ election=election,
+ name=candidate[0],
+ url=candidate[1])
+ SESSION.add(cand)
+ candidates_name.append(cand.name)
+ else:
+ flask.flash("There was an issue!")
+ fedmsgshim.publish(
+ topic="candidate.new",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=cand.election.to_json(),
+ candidate=cand.to_json(),
+ )
+ )
+
+ SESSION.commit()
+ flask.flash('Added %s candidates' % len(candidates_name))
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+
+ return flask.render_template(
+ 'admin/candidate_multi.html',
+ form=form,
+ submit_text='Add candidates')
+
+
+(a)APP.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
+ methods=('GET', 'POST'))
+@election_admin_required
+def admin_edit_candidate(election_alias, candidate_id):
+ election = models.Election.get(SESSION, alias=election_alias)
+ candidate = models.Candidate.get(SESSION, candidate_id=candidate_id)
+
+ if not election or not candidate:
+ flask.abort(404)
+
+ if candidate.election != election:
+ flask.abort(404)
+
+ form = forms.CandidateForm(obj=candidate)
+ if form.validate_on_submit():
+ form.populate_obj(candidate)
+ SESSION.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ fedmsgshim.publish(
+ topic="candidate.edit",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=candidate.election.to_json(),
+ candidate=candidate.to_json(),
+ )
+ )
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+
+ return flask.render_template(
+ 'admin/candidate.html',
+ form=form,
+ submit_text='Edit candidate')
+
+
+(a)APP.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
+ methods=('GET', 'POST'))
+@election_admin_required
+def admin_delete_candidate(election_alias, candidate_id):
+ election = models.Election.get(SESSION, alias=election_alias)
+ candidate = models.Candidate.get(SESSION, candidate_id=candidate_id)
+
+ if not election or not candidate:
+ flask.abort(404)
+
+ if candidate.election != election:
+ flask.abort(404)
+
+ form = forms.ConfirmationForm()
+ if form.validate_on_submit():
+ candidate_name = candidate.name
+ SESSION.delete(candidate)
+ SESSION.commit()
+ flask.flash('Candidate "%s" deleted' % candidate_name)
+ fedmsgshim.publish(
+ topic="candidate.delete",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=candidate.election.to_json(),
+ candidate=candidate.to_json(),
+ )
+ )
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+
+ return flask.render_template(
+ 'admin/delete_candidate.html',
+ form=form,
+ candidate=candidate)
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
new file mode 100644
index 0000000..b12c6c7
--- /dev/null
+++ b/fedora_elections/elections.py
@@ -0,0 +1,317 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2012 Red Hat, Inc.
+# Copyright © 2012 Ian Weller <ianweller(a)fedoraproject.org>
+# Copyright © 2012 Toshio Kuratomi <tkuratom(a)redhat.com>
+# Copyright © 2012 Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+#
+# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
+# Toshio Kuratomi <tkuratom(a)redhat.com>
+# Frank Chiulli <fchiulli(a)fedoraproject.org>
+# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
+#
+
+from datetime import datetime, time
+from functools import wraps
+
+import flask
+from sqlalchemy.orm.exc import NoResultFound
+
+from fedora_elections import fedmsgshim
+from fedora_elections import forms
+from fedora_elections import models
+from fedora_elections import APP, SESSION, is_authenticated, is_admin,\
+ is_safe_url, safe_redirect_back
+
+
+def login_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if not is_authenticated():
+ return flask.redirect(flask.url_for(
+ 'auth_login', next=flask.request.url))
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+def get_valid_election(election_alias, ended=False):
+ """ Return the election if it is valid (no pending, not ended).
+ """
+ election = models.Election.get(SESSION, alias=election_alias)
+
+ if not election:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return safe_redirect_back()
+
+ if election.status == 'Pending':
+ flask.flash('Voting has not yet started, sorry.')
+ return safe_redirect_back()
+
+ elif not ended and election.status == 'Ended':
+ flask.flash(
+ 'This election is closed. You have been redirected to the '
+ 'election results.')
+ return flask.redirect(flask.url_for(
+ 'election_results', election_alias=election.alias))
+
+ elif ended and election.status == 'In progress':
+ flask.flash(
+ 'Sorry but this election is in progress, you may not see its '
+ 'results already.')
+ return safe_redirect_back()
+
+ return election
+
+
+
+(a)APP.route('/vote/<election_alias>', methods=['GET', 'POST'])
+@login_required
+def vote(election_alias):
+ election = get_valid_election(election_alias)
+
+ if not isinstance(election, models.Election):
+ return election
+
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, election.id, count=True)
+
+ if votes > 0:
+ flask.flash('You have already voted in the election!')
+ return safe_redirect_back()
+
+ if election.voting_type == 'range':
+ return vote_range(election_alias)
+ elif election.voting_type == 'simple':
+ return vote_simple(election_alias)
+ else:
+ flask.flash(
+ 'Unknown election voting type: %s' % election.voting_type)
+ return safe_redirect_back()
+
+
+(a)APP.route('/vote_range/<election_alias>', methods=['GET', 'POST'])
+@login_required
+def vote_range(election_alias):
+ election = get_valid_election(election_alias)
+
+ if not isinstance(election, models.Election):
+ return election
+
+ if (election.voting_type == 'simple'):
+ return flask.redirect(flask.url_for(
+ 'vote_simple', election_alias=election_alias))
+
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, election.id, count=True)
+
+ if votes > 0:
+ flask.flash('You have already voted in the election!')
+ return safe_redirect_back()
+
+ num_candidates = election.candidates.count()
+
+ uvotes = {}
+ for candidate in election.candidates:
+ uvotes[candidate.id] = ""
+ next_action = 'vote'
+
+ if flask.request.method == 'POST':
+ next_action = ''
+ form_values = flask.request.form.values()
+ if 'Submit' in form_values:
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote >= 0 and vote <= num_candidates:
+ uvotes[candidate.id] = int(vote)
+ else:
+ flask.flash("Invalid Ballot!")
+ return safe_redirect_back()
+
+ except ValueError:
+ flask.flash("Invalid Ballot!")
+ return safe_redirect_back()
+
+ for uvote in uvotes:
+ new_vote = models.Vote(
+ election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=uvote,
+ value=uvotes[uvote]
+ )
+ SESSION.add(new_vote)
+ SESSION.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return safe_redirect_back()
+
+ elif 'Preview' in form_values:
+ flask.flash("Please confirm your vote!")
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote > num_candidates:
+ flask.flash("Invalid data.")
+ uvotes[candidate.id] = 0
+ next_action = 'vote'
+ elif vote >= 0:
+ uvotes[candidate.id] = vote
+ else:
+ flask.flash("Invalid data2.")
+ uvotes[c.id] = 0
+ next_action = 'vote'
+ except ValueError:
+ flask.flash("Invalid data")
+
+ if (next_action != 'vote'):
+ next_action = 'confirm'
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ FAS2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template(
+ 'vote_range.html',
+ election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ voteinfo=uvotes,
+ nextaction=next_action)
+
+
+(a)APP.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
+@login_required
+def vote_simple(election_alias):
+ election = get_valid_election(election_alias)
+
+ if not isinstance(election, models.Election):
+ return election
+
+ if (election.voting_type == 'range'):
+ return flask.redirect(flask.url_for(
+ 'vote_range', election_alias=election_alias))
+
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, election.id, count=True)
+
+ if (votes != 0):
+ flask.flash('You have already voted in the election!')
+ return safe_redirect_back()
+
+ num_candidates = election.candidates.count()
+ next_action = 'vote'
+ candidate_id = -1
+
+ if flask.request.method == 'POST':
+ next_action = ''
+ form_values = flask.request.form.values()
+ if 'Submit' in form_values:
+ candidate_id = int(flask.request.form['candidate'])
+ new_vote = models.Vote(
+ election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=candidate_id,
+ value=1
+ )
+ SESSION.add(new_vote)
+ SESSION.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return safe_redirect_back()
+
+ elif 'Preview' in form_values:
+ if ('candidate' in flask.request.form):
+ flask.flash("Please confirm your vote!")
+ candidate_id = int(flask.request.form['candidate'])
+ else:
+ flask.flash("Please vote for a candidate")
+ next_action = 'vote'
+
+ if (next_action != 'vote'):
+ next_action = 'confirm'
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ FAS2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template(
+ 'vote_simple.html',
+ election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ candidate_id=candidate_id,
+ nextaction=next_action)
+
+
+(a)APP.route('/results/<election_alias>')
+def election_results(election_alias):
+ election = get_valid_election(election_alias, ended=True)
+
+ if not isinstance(election, models.Election):
+ return election
+
+ elif election.embargoed:
+ if not hasattr(flask.g, 'fas_user') or not flask.g.fas_user:
+ flask.flash("We are sorry. The results for this election"
+ "cannot be viewed because they are currently "
+ "embargoed pending formal announcement.")
+ return safe_redirect_back()
+ else:
+ if is_admin(flask.g.fas_user) \
+ or is_election_admin(flask.g.fas_user, election.id):
+ flask.flash("The results for this election are currently "
+ "embargoed pending formal announcement.",
+ "error")
+ pass
+ else:
+ flask.flash(
+ "We are sorry. The results for this election"
+ "cannot be viewed because they are currently "
+ "embargoed pending formal announcement.")
+ return safe_redirect_back()
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ FAS2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ stats = models.Vote.get_election_stats(SESSION, election.id)
+
+ return flask.render_template(
+ 'results.html',
+ election=election,
+ usernamemap=usernamemap,
+ stats=stats,
+ )
commit be1e7563a203cbe1961b6cd04bbdf04f727f2469
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 10:34:20 2014 +0200
Remove the redirect.py file
diff --git a/fedora_elections/redirect.py b/fedora_elections/redirect.py
deleted file mode 100644
index e7aa43e..0000000
--- a/fedora_elections/redirect.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import flask
-
-from urlparse import urljoin, urlparse
-
-
-def is_safe_url(target):
- ref_url = urlparse(flask.request.host_url)
- test_url = urlparse(urljoin(flask.request.host_url, target))
- return test_url.scheme in ('http', 'https') and \
- ref_url.netloc == test_url.netloc
-
-
-def safe_redirect_back(next=None, fallback=('index', {})):
- targets = []
- if next:
- targets.append(next)
- if 'next' in flask.request.args and \
- flask.request.args['next']:
- targets.append(flask.request.args['next'])
- targets.append(flask.url_for(fallback[0], **fallback[1]))
- for target in targets:
- if is_safe_url(target):
- return flask.redirect(target)
commit ecb63302f29fdb1da9d89b18517e92a639a84b94
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 09:46:06 2014 +0200
Adjust the flask application for the new template paths
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 88d0e2e..cfe4695 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -199,7 +199,7 @@ def index():
voted.append(elec)
return flask.render_template(
- 'list/index.html',
+ 'index.html',
prev_elections=prev_elections,
cur_elections=cur_elections,
next_elections=next_elections,
@@ -227,7 +227,7 @@ def about_election(election_alias):
usernamemap[candidate.id] = candidate.name
return flask.render_template(
- 'election/about.html',
+ 'about.html',
election=election,
usernamemap=usernamemap)
@@ -243,7 +243,7 @@ def archived_elections():
return redirect.safe_redirect_back()
return flask.render_template(
- 'election/archive.html',
+ 'archive.html',
elections=elections)
@@ -266,7 +266,7 @@ def open_elections():
voted.append(elec)
return flask.render_template(
- 'list/index.html',
+ 'index.html',
next_elections=elections,
voted=voted,
tag='open',
@@ -389,7 +389,7 @@ def vote_range(election_alias):
usernamemap[candidate.id] = candidate.name
return flask.render_template(
- 'election/vote_range.html',
+ 'vote_range.html',
election=election,
num_candidates=num_candidates,
usernamemap=usernamemap,
@@ -460,7 +460,7 @@ def vote_simple(election_alias):
usernamemap[candidate.id] = candidate.name
return flask.render_template(
- 'election/vote_simple.html',
+ 'vote_simple.html',
election=election,
num_candidates=num_candidates,
usernamemap=usernamemap,
@@ -797,7 +797,7 @@ def election_results(election_alias):
stats = models.Vote.get_election_stats(SESSION, election.id)
return flask.render_template(
- 'election/results.html',
+ 'results.html',
election=election,
usernamemap=usernamemap,
stats=stats,
commit 1d3eb8b6d92a02a44d923a370b3a7c3d200aaaef
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 09:44:09 2014 +0200
Move the templates in the `election` folder one level up
diff --git a/fedora_elections/templates/about.html b/fedora_elections/templates/about.html
new file mode 100644
index 0000000..7a604bb
--- /dev/null
+++ b/fedora_elections/templates/about.html
@@ -0,0 +1,40 @@
+{% extends "base.html" %}
+{% block title %}Election Information{% endblock %}
+{% block body %}
+ <h1>{{ election.shortdesc }}</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ {% if election.status == 'Ended' %}
+ <a href="/results/{{ election.alias }}">Show Results!</a>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p />
+ {% endif %}
+{% endblock %}
diff --git a/fedora_elections/templates/archive.html b/fedora_elections/templates/archive.html
new file mode 100644
index 0000000..f0d539a
--- /dev/null
+++ b/fedora_elections/templates/archive.html
@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+
+{%block tag %}archives{% endblock %}
+
+{% block body %}
+ <h2>Past Elections</h2>
+ <table border="1" cellpadding="1">
+ <tr>
+ <th></th>
+ <th>Start Date (UTC)</th>
+ <th>End Date (UTC)</th>
+ <th></th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{election.description}}</td>
+ <td>{{election.start_date}}</td>
+ <td>{{election.end_date}}</td>
+ <td>
+ <a href="about/{{ election.alias }}">More information</a>
+ </td>
+ <td>
+ <a href="results/{{ election.alias }}">Results</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+{% endblock %}
diff --git a/fedora_elections/templates/election/about.html b/fedora_elections/templates/election/about.html
deleted file mode 100644
index 7a604bb..0000000
--- a/fedora_elections/templates/election/about.html
+++ /dev/null
@@ -1,40 +0,0 @@
-{% extends "base.html" %}
-{% block title %}Election Information{% endblock %}
-{% block body %}
- <h1>{{ election.shortdesc }}</h1>
- <p>{{ election.description }}</p>
- {% if election.url %}
- <p><a href="{{ election.url }}">[More Information]</a></p>
- {% endif %}
- {% if election.status == 'Ended' %}
- <a href="/results/{{ election.alias }}">Show Results!</a>
- {% endif %}
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- {% if candidate.url %}
- <small><a href="{{ candidate.url }}">[info]</a></small>
- {% endif %}
- </td>
- </tr>
- {% endfor %}
- </table>
- {% if election.legal_voters.count() != 0 %}
- <p>
- To vote in this election you must be a member of any one of the following groups:
- </p>
- <ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
- </ul>
- {% else %}
- <p />
- {% endif %}
-{% endblock %}
diff --git a/fedora_elections/templates/election/archive.html b/fedora_elections/templates/election/archive.html
deleted file mode 100644
index f0d539a..0000000
--- a/fedora_elections/templates/election/archive.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{% extends "base.html" %}
-
-{%block tag %}archives{% endblock %}
-
-{% block body %}
- <h2>Past Elections</h2>
- <table border="1" cellpadding="1">
- <tr>
- <th></th>
- <th>Start Date (UTC)</th>
- <th>End Date (UTC)</th>
- <th></th>
- <th></th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>{{election.description}}</td>
- <td>{{election.start_date}}</td>
- <td>{{election.end_date}}</td>
- <td>
- <a href="about/{{ election.alias }}">More information</a>
- </td>
- <td>
- <a href="results/{{ election.alias }}">Results</a>
- </td>
- </tr>
- {% endfor %}
- </table>
-{% endblock %}
diff --git a/fedora_elections/templates/election/results.html b/fedora_elections/templates/election/results.html
deleted file mode 100644
index cc5e849..0000000
--- a/fedora_elections/templates/election/results.html
+++ /dev/null
@@ -1,65 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- <h1>{{ election.shortdesc }} Results</h1>
- <p>{{ election.description }}</p>
- {% if election.url %}
- <p><a href="{{ election.url }}">[More Information]</a></p>
- {% endif %}
- <table border="1" cellpadding="1">
- <tr>
- <th>Candidate</th>
- <th title="Number of votes received">Votes</th>
- </tr>
- {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{candidate.name}}
- {% endif %}
- {% if candidate.url %}
- <small><a href="{{ candidate.url }}">[info]</a></small>
- {% endif %}
- </td>
- <td>{{ candidate.vote_count }}</td>
- </tr>
- {% endfor %}
- </table>
-
- <section class="stats">
- <h3>Some statistics about this election</h3>
- <table>
- <tr>
- <th>Number of voters</th>
- <td>{{ stats['n_voters'] }}</td>
- </tr>
- <tr>
- <th title="Number of votes registered, there may be more than one vote per voters">Number of votes</th>
- <td>{{ stats['n_votes'] }}</td>
- </tr>
- <tr>
- <th title="Maximum number of votes possible">Maximum of votes</th>
- <td>{{ stats['n_voters'] * stats['n_candidates'] }}</td>
- </tr>
- {% if stats['candidate_voters'] %}
- <tr>
- <th></th>
- <th>Number of voters per candidate</th>
- <th>Average votes per candidate</th>
- </tr>
- {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
- <tr>
- <th>{{ candidate.name }}</th>
- <td>{{ stats['candidate_voters'][candidate.name] }}</td>
- {% if stats['candidate_voters'][candidate.name] %}
- <td>{{ candidate.vote_count / stats['candidate_voters'][candidate.name] }}</td>
- {% else %}
- <td> - </td>
- {% endif %}
- </tr>
- {% endfor %}
- {% endif %}
- </table>
- </section>
-{% endblock %}
diff --git a/fedora_elections/templates/election/vote_range.html b/fedora_elections/templates/election/vote_range.html
deleted file mode 100644
index 71a7ee1..0000000
--- a/fedora_elections/templates/election/vote_range.html
+++ /dev/null
@@ -1,116 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-<h2>{{election.shortdesc}}</h2>
-
-<p>{{election.description}}</p>
-
-<p><a href="{{election.url}}">[More Information]</a></p>
-
-<form action="" method="post">
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- <a href="{{candidate.url}}">[Info]</a>
- </td>
- <!-- If user is logged in: -->
- {% if g.fas_user %}
- {% if nextaction == 'vote' %}
- <td>
- <select name="{{candidate.id}}">
- {% for opt in range(0, num_candidates+1) %}
- <option value={{opt}}>{{opt}}</option>
- {% endfor %}
- </select>
- </td>
- {% endif %}
- {% if nextaction == 'confirm' %}
- <td>
- {% set cid = candidate.id %}
- <input type="hidden" name="{{candidate.id}}"
- value="{{voteinfo[cid]}}" />
- {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
- {% if weightpercent >= 90 %}
- <div class="vthot">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 70 %}
- <div class="vtwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 50 %}
- <div class="vtmedwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 30 %}
- <div class="vtmedcool">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 10 %}
- <div class="vtcool">
- {{voteinfo[cid]}}
- </div>
- {% else %}
- <div class="vtcold">
- {{voteinfo[cid]}}
- </div>
- {% endif %}
- </td>
- {% endif %}
- {% endif %}
- <!-- End if user logged in -->
- </tr>
- {% endfor %}
- </table>
- {% if g.fas_user %}
- <p>
- {% if nextaction == 'confirm' %}
- <input type="submit" name="{{nextaction}}" value="Submit" />
- {% else %}
- <input type="submit" name="{{nextaction}}" value="Preview" />
- {% endif %}
- </p>
- {% endif %}
-</form>
-
-<!-- Make this conditional on whether the user is logged in and can vote in the election -->
-{% if election.legal_voters.count() != 0 %}
- <p>
- To vote in this election you must be a member of any one of the following groups:
- </p>
- <ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
- </ul>
-{% else %}
- <p />
-{% endif %}
-<div>
- <h2>Information on Range Voting</h2>
- <p>
- Fedora Project has implemented
- <a href="http://rangevoting.org/">Range Voting</a>
- for this election, in particular the "Range (score-summing, blanks
- treated as zero score, no quorum rule)" range voting system.
- </p>
- <p>
- To cast your vote in this election simply select a value between 0 and
- {{num_candidates}} with 0 as 'least or no preference' and
- {{num_candidates}} as 'highest preference'.
- </p>
- <p>
- At the end of the election, the highest ranking candidate(s) are marked
- as the winners.
- </p>
- <p>
- For more information about Range Voting, visit the
- <a href="http://rangevoting.org/">Center for Range Voting</a>.
- </p>
-</div>
-{% endblock %}
diff --git a/fedora_elections/templates/election/vote_simple.html b/fedora_elections/templates/election/vote_simple.html
deleted file mode 100644
index 0d43183..0000000
--- a/fedora_elections/templates/election/vote_simple.html
+++ /dev/null
@@ -1,53 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-<h2>{{election.shortdesc}}</h2>
-
-<p>{{election.description}}</p>
-
-<p><a href="{{election.url}}">[More Information]</a></p>
-
-<form action="" method="post">
- <table border="0" cellpadding="1">
- {% for candidate in election.candidates|sort %}
- <tr>
- <td>
- {% if candidate.id == candidate_id %}
- <input type="radio" name="candidate" value="{{ candidate.id }}" checked="checked">
- {% else %}
- <input type="radio" name="candidate" value="{{ candidate.id }}">
- {% endif %}
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- <a href="{{candidate.url}}">[Info]</a>
- </td>
- </tr>
- {% endfor %}
- </table>
- {% if g.fas_user %}
- <p>
- {% if nextaction == 'confirm' %}
- <input type="submit" name="{{nextaction}}" value="Submit" />
- {% else %}
- <input type="submit" name="{{nextaction}}" value="Preview" />
- {% endif %}
- </p>
- {% endif %}
-</form>
-
-<!-- Make this conditional on whether the user is logged in and can vote in the election -->
-{% if election.legal_voters.count() != 0 %}
- <p>
- To vote in this election you must be a member of any one of the following groups:
- </p>
- <ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
- </ul>
-{% else %}
- <p />
-{% endif %}
-{% endblock %}
diff --git a/fedora_elections/templates/results.html b/fedora_elections/templates/results.html
new file mode 100644
index 0000000..cc5e849
--- /dev/null
+++ b/fedora_elections/templates/results.html
@@ -0,0 +1,65 @@
+{% extends "base.html" %}
+{% block body %}
+ <h1>{{ election.shortdesc }} Results</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ <tr>
+ <th>Candidate</th>
+ <th title="Number of votes received">Votes</th>
+ </tr>
+ {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{candidate.name}}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ <td>{{ candidate.vote_count }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+
+ <section class="stats">
+ <h3>Some statistics about this election</h3>
+ <table>
+ <tr>
+ <th>Number of voters</th>
+ <td>{{ stats['n_voters'] }}</td>
+ </tr>
+ <tr>
+ <th title="Number of votes registered, there may be more than one vote per voters">Number of votes</th>
+ <td>{{ stats['n_votes'] }}</td>
+ </tr>
+ <tr>
+ <th title="Maximum number of votes possible">Maximum of votes</th>
+ <td>{{ stats['n_voters'] * stats['n_candidates'] }}</td>
+ </tr>
+ {% if stats['candidate_voters'] %}
+ <tr>
+ <th></th>
+ <th>Number of voters per candidate</th>
+ <th>Average votes per candidate</th>
+ </tr>
+ {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
+ <tr>
+ <th>{{ candidate.name }}</th>
+ <td>{{ stats['candidate_voters'][candidate.name] }}</td>
+ {% if stats['candidate_voters'][candidate.name] %}
+ <td>{{ candidate.vote_count / stats['candidate_voters'][candidate.name] }}</td>
+ {% else %}
+ <td> - </td>
+ {% endif %}
+ </tr>
+ {% endfor %}
+ {% endif %}
+ </table>
+ </section>
+{% endblock %}
diff --git a/fedora_elections/templates/vote_range.html b/fedora_elections/templates/vote_range.html
new file mode 100644
index 0000000..71a7ee1
--- /dev/null
+++ b/fedora_elections/templates/vote_range.html
@@ -0,0 +1,116 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{election.shortdesc}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
+<form action="" method="post">
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ <a href="{{candidate.url}}">[Info]</a>
+ </td>
+ <!-- If user is logged in: -->
+ {% if g.fas_user %}
+ {% if nextaction == 'vote' %}
+ <td>
+ <select name="{{candidate.id}}">
+ {% for opt in range(0, num_candidates+1) %}
+ <option value={{opt}}>{{opt}}</option>
+ {% endfor %}
+ </select>
+ </td>
+ {% endif %}
+ {% if nextaction == 'confirm' %}
+ <td>
+ {% set cid = candidate.id %}
+ <input type="hidden" name="{{candidate.id}}"
+ value="{{voteinfo[cid]}}" />
+ {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
+ {% if weightpercent >= 90 %}
+ <div class="vthot">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 70 %}
+ <div class="vtwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 50 %}
+ <div class="vtmedwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 30 %}
+ <div class="vtmedcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 10 %}
+ <div class="vtcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% else %}
+ <div class="vtcold">
+ {{voteinfo[cid]}}
+ </div>
+ {% endif %}
+ </td>
+ {% endif %}
+ {% endif %}
+ <!-- End if user logged in -->
+ </tr>
+ {% endfor %}
+ </table>
+ {% if g.fas_user %}
+ <p>
+ {% if nextaction == 'confirm' %}
+ <input type="submit" name="{{nextaction}}" value="Submit" />
+ {% else %}
+ <input type="submit" name="{{nextaction}}" value="Preview" />
+ {% endif %}
+ </p>
+ {% endif %}
+</form>
+
+<!-- Make this conditional on whether the user is logged in and can vote in the election -->
+{% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+{% else %}
+ <p />
+{% endif %}
+<div>
+ <h2>Information on Range Voting</h2>
+ <p>
+ Fedora Project has implemented
+ <a href="http://rangevoting.org/">Range Voting</a>
+ for this election, in particular the "Range (score-summing, blanks
+ treated as zero score, no quorum rule)" range voting system.
+ </p>
+ <p>
+ To cast your vote in this election simply select a value between 0 and
+ {{num_candidates}} with 0 as 'least or no preference' and
+ {{num_candidates}} as 'highest preference'.
+ </p>
+ <p>
+ At the end of the election, the highest ranking candidate(s) are marked
+ as the winners.
+ </p>
+ <p>
+ For more information about Range Voting, visit the
+ <a href="http://rangevoting.org/">Center for Range Voting</a>.
+ </p>
+</div>
+{% endblock %}
diff --git a/fedora_elections/templates/vote_simple.html b/fedora_elections/templates/vote_simple.html
new file mode 100644
index 0000000..0d43183
--- /dev/null
+++ b/fedora_elections/templates/vote_simple.html
@@ -0,0 +1,53 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{election.shortdesc}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
+<form action="" method="post">
+ <table border="0" cellpadding="1">
+ {% for candidate in election.candidates|sort %}
+ <tr>
+ <td>
+ {% if candidate.id == candidate_id %}
+ <input type="radio" name="candidate" value="{{ candidate.id }}" checked="checked">
+ {% else %}
+ <input type="radio" name="candidate" value="{{ candidate.id }}">
+ {% endif %}
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ <a href="{{candidate.url}}">[Info]</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% if g.fas_user %}
+ <p>
+ {% if nextaction == 'confirm' %}
+ <input type="submit" name="{{nextaction}}" value="Submit" />
+ {% else %}
+ <input type="submit" name="{{nextaction}}" value="Preview" />
+ {% endif %}
+ </p>
+ {% endif %}
+</form>
+
+<!-- Make this conditional on whether the user is logged in and can vote in the election -->
+{% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+{% else %}
+ <p />
+{% endif %}
+{% endblock %}
commit 85ef2ed4e5cf7737d20aa1e52224f67b58c2f913
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 09:43:46 2014 +0200
Move the index template one level up
diff --git a/fedora_elections/templates/index.html b/fedora_elections/templates/index.html
new file mode 100644
index 0000000..8f70756
--- /dev/null
+++ b/fedora_elections/templates/index.html
@@ -0,0 +1,70 @@
+{% extends "base.html" %}
+
+{%block tag %}{{tag}}{% endblock %}
+
+{% macro render_elections(election_list, title) -%}
+
+ <h3>{{title}}</h3>
+
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ {% for election in election_list %}
+ <tr>
+ <td>{{ election.shortdesc }}</td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ <td>
+ {% if election.status == "In progress" %}
+ {% if election not in voted %}
+ {% if election.voting_type == 'range' %}
+ <a href="{{ url_for('vote_range', election_alias=election.alias) }}">
+ {% elif election.voting_type == 'simple' %}
+ <a href="{{ url_for('vote_simple', election_alias=election.alias) }}">
+ {% endif %}
+ Vote now!
+ </a>
+ {% endif %}
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+
+{% endmacro %}
+
+{% block body %}
+ <h2>{{ title }}</h2>
+
+ {% if tag == 'index' %}
+ <p>
+ The <a href='https://fedoraproject.org'>Fedora project</a> is a community
+ driven project. It has a number of steering commities whose responsabilities
+ is to drive the project forward.
+ </p>
+ <p>
+ These committies are elected among volunteers of the community and by
+ the community members. The present application is meant to held these
+ elections.
+ </p>
+ {% endif %}
+
+ {% if cur_elections %}
+ {{ render_elections(cur_elections, 'Current elections') }}
+ {% endif %}
+
+ {% if next_elections %}
+ {{ render_elections(next_elections, 'Next %s elections' % (next_elections|length)) }}
+ {% endif %}
+
+ {% if prev_elections %}
+ {{ render_elections(prev_elections, 'Last %s elections' % (prev_elections|length)) }}
+ {% endif %}
+
+{% endblock %}
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
deleted file mode 100644
index 8f70756..0000000
--- a/fedora_elections/templates/list/index.html
+++ /dev/null
@@ -1,70 +0,0 @@
-{% extends "base.html" %}
-
-{%block tag %}{{tag}}{% endblock %}
-
-{% macro render_elections(election_list, title) -%}
-
- <h3>{{title}}</h3>
-
- <table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- <th></th>
- </tr>
- {% for election in election_list %}
- <tr>
- <td>{{ election.shortdesc }}</td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- <td>
- {% if election.status == "In progress" %}
- {% if election not in voted %}
- {% if election.voting_type == 'range' %}
- <a href="{{ url_for('vote_range', election_alias=election.alias) }}">
- {% elif election.voting_type == 'simple' %}
- <a href="{{ url_for('vote_simple', election_alias=election.alias) }}">
- {% endif %}
- Vote now!
- </a>
- {% endif %}
- {% endif %}
- </td>
- </tr>
- {% endfor %}
- </table>
-
-{% endmacro %}
-
-{% block body %}
- <h2>{{ title }}</h2>
-
- {% if tag == 'index' %}
- <p>
- The <a href='https://fedoraproject.org'>Fedora project</a> is a community
- driven project. It has a number of steering commities whose responsabilities
- is to drive the project forward.
- </p>
- <p>
- These committies are elected among volunteers of the community and by
- the community members. The present application is meant to held these
- elections.
- </p>
- {% endif %}
-
- {% if cur_elections %}
- {{ render_elections(cur_elections, 'Current elections') }}
- {% endif %}
-
- {% if next_elections %}
- {{ render_elections(next_elections, 'Next %s elections' % (next_elections|length)) }}
- {% endif %}
-
- {% if prev_elections %}
- {{ render_elections(prev_elections, 'Last %s elections' % (prev_elections|length)) }}
- {% endif %}
-
-{% endblock %}
commit 2ddfc7c5d230a9f76ed65505cf92230a5effc3e5
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 10 13:49:47 2014 +0200
Publish fedmsg messages upon adding, editing and deleting candidates
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 03e6a12..88d0e2e 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -618,6 +618,14 @@ def admin_add_candidate(election_alias):
SESSION.add(candidate)
SESSION.commit()
flask.flash('Candidate "%s" saved' % candidate.name)
+ fedmsgshim.publish(
+ topic="candidate.new",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=candidate.election.to_json(),
+ candidate=candidate.to_json(),
+ )
+ )
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
@@ -658,6 +666,14 @@ def admin_add_multi_candidate(election_alias):
candidates_name.append(cand.name)
else:
flask.flash("There was an issue!")
+ fedmsgshim.publish(
+ topic="candidate.new",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=cand.election.to_json(),
+ candidate=cand.to_json(),
+ )
+ )
SESSION.commit()
flask.flash('Added %s candidates' % len(candidates_name))
@@ -688,6 +704,14 @@ def admin_edit_candidate(election_alias, candidate_id):
form.populate_obj(candidate)
SESSION.commit()
flask.flash('Candidate "%s" saved' % candidate.name)
+ fedmsgshim.publish(
+ topic="candidate.edit",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=candidate.election.to_json(),
+ candidate=candidate.to_json(),
+ )
+ )
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
@@ -716,6 +740,14 @@ def admin_delete_candidate(election_alias, candidate_id):
SESSION.delete(candidate)
SESSION.commit()
flask.flash('Candidate "%s" deleted' % candidate_name)
+ fedmsgshim.publish(
+ topic="candidate.delete",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=candidate.election.to_json(),
+ candidate=candidate.to_json(),
+ )
+ )
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
commit da7b095d4aaca9f6219697f76c1d2c7d560ebd48
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 10 13:49:23 2014 +0200
Add method to convert a Candidate object into a json representation of it
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 0779042..4c4e300 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -236,6 +236,13 @@ class Candidate(BASE):
name = sa.Column(sa.Unicode(150), nullable=False)
url = sa.Column(sa.Unicode(250))
+ def to_json(self):
+ ''' Return a json representation of this object. '''
+ return dict(
+ name=self.name,
+ url=self.url,
+ )
+
@property
def vote_count(self):
values = [v.value for v in self.votes]
commit 9c68bc6b96c44dff47881351ccb07338164e8f0a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 10 13:28:13 2014 +0200
Fix the conversion of the election objects into a json structure for fedmsg
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 6eda2ee..03e6a12 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -547,7 +547,7 @@ def admin_new_election():
topic="election.new",
msg=dict(
agent=flask.g.fas_user.username,
- election=election.api_repr(version=1),
+ election=election.to_json(),
)
)
flask.flash('Election "%s" added' % election.alias)
@@ -587,7 +587,7 @@ def admin_edit_election(election_alias):
topic="election.edit",
msg=dict(
agent=flask.g.fas_user.username,
- election=election.api_repr(version=1),
+ election=election.to_json(),
)
)
flask.flash('Election "%s" saved' % election.alias)
commit c09dd8a10d953919a77770eaf5491f9de5b2d531
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 19:47:44 2014 +0200
Broadcast on fedmsg when an election is added or edited
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 0af7401..6eda2ee 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -543,8 +543,14 @@ def admin_new_election():
SESSION.commit()
+ fedmsgshim.publish(
+ topic="election.new",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=election.api_repr(version=1),
+ )
+ )
flask.flash('Election "%s" added' % election.alias)
- fedmsgshim.publish(topic="election.new", msg=election)
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
return flask.render_template(
@@ -577,6 +583,13 @@ def admin_edit_election(election_alias):
form.embargoed.data = int(form.embargoed.data)
form.populate_obj(election)
SESSION.commit()
+ fedmsgshim.publish(
+ topic="election.edit",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=election.api_repr(version=1),
+ )
+ )
flask.flash('Election "%s" saved' % election.alias)
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
commit 143ab9f1c441d798afbe16f399abc284312f4b0c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 19:47:24 2014 +0200
Add a json compatible representation of an election object
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 0c55e49..0779042 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -97,6 +97,19 @@ class Election(BASE):
default=False)
fas_user = sa.Column(sa.Unicode(50), nullable=False)
+ def to_json(self):
+ ''' Return a json representation of this object. '''
+ return dict(
+ shortdesc=self.shortdesc,
+ alias=self.alias,
+ description=self.description,
+ url=self.url,
+ start_date=self.start_date,
+ end_date=self.end_date,
+ embargoed=self.embargoed,
+ voting_type=self.voting_type,
+ )
+
@property
def status(self):
now = datetime.utcnow()
commit 769522c4193da031e4e43b3f77f0488dc2c744d2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 19:47:07 2014 +0200
Update the content of the fedmsg.d folder, simplify it
diff --git a/fedmsg.d/base.py b/fedmsg.d/base.py
deleted file mode 100644
index 581c207..0000000
--- a/fedmsg.d/base.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# This file is part of fedmsg.
-# Copyright (C) 2012 Red Hat, Inc.
-#
-# fedmsg is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# fedmsg is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with fedmsg; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-#
-# Authors: Ralph Bean <rbean(a)redhat.com>
-#
-config = dict(
- # Set this to dev if you're hacking on fedmsg or an app.
- # Set to stg or prod if running in the Fedora Infrastructure
- environment="dev",
-
- # Default is 0
- high_water_mark=0,
- io_threads=1,
-
- ## For the fedmsg-hub and fedmsg-relay. ##
-
- # We almost always want the fedmsg-hub to be sending messages with zmq as
- # opposed to amqp or stomp.
- zmq_enabled=True,
-
- # When subscribing to messages, we want to allow splats ('*') so we tell
- # the hub to not be strict when comparing messages topics to subscription
- # topics.
- zmq_strict=False,
-)
diff --git a/fedmsg.d/elections.py b/fedmsg.d/elections.py
new file mode 100644
index 0000000..385697b
--- /dev/null
+++ b/fedmsg.d/elections.py
@@ -0,0 +1,20 @@
+""" This is an example pkgdb configuration for fedmsg.
+By convention, it is normally installed as ``/etc/fedmsg.d/pkgdb.py``
+
+For Fedora Infrastructure, our own version of this file is kept in
+``puppet/modules/fedmsg/templates/fedmsg.d/``
+
+It needs to be globally available so remote consumers know how to find the
+pkgdb producer (wsgi process).
+"""
+
+import socket
+hostname = socket.gethostname().split('.')[0]
+
+config = dict(
+ endpoints={
+ "fedora_elections.%s" % hostname: [
+ "tcp://127.0.0.1:3005",
+ ],
+ },
+)
diff --git a/fedmsg.d/endpoints.py b/fedmsg.d/endpoints.py
deleted file mode 100644
index 88b37c0..0000000
--- a/fedmsg.d/endpoints.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# This file is part of fedmsg.
-# Copyright (C) 2012 Red Hat, Inc.
-#
-# fedmsg is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# fedmsg is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with fedmsg; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-#
-# Authors: Ralph Bean <rbean(a)redhat.com>
-#
-import socket
-hostname = socket.gethostname()
-
-config = dict(
- # This is a dict of possible addresses from which fedmsg can send
- # messages. fedmsg.init(...) requires that a 'name' argument be passed
- # to it which corresponds with one of the keys in this dict.
- endpoints={
- # These are here so your local box can listen to the upstream
- # infrastructure's bus. Cool, right? :)
- "fedora-infrastructure": [
- "tcp://fedoraproject.org:9940",
- "tcp://stg.fedoraproject.org:9940",
- ],
-
- # This is the output side of the relay to which all other
- # services can listen.
- "relay_outbound": ["tcp://*:4001"],
-
- # And this is what our app actually uses.
- "fedora_elections.%s" % hostname: ["tcp://*:3007"],
- },
-
- relay_inbound="tcp://127.0.0.1:2003",
-)
diff --git a/fedmsg.d/ssl.py b/fedmsg.d/ssl.py
deleted file mode 100644
index a286c77..0000000
--- a/fedmsg.d/ssl.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# This file is part of fedmsg.
-# Copyright (C) 2012 Red Hat, Inc.
-#
-# fedmsg is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# fedmsg is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with fedmsg; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-#
-# Authors: Ralph Bean <rbean(a)redhat.com>
-#
-import os
-import socket
-
-SEP = os.path.sep
-here = os.getcwd()
-hostname = socket.gethostname()
-
-config = dict(
- sign_messages=False,
- validate_signatures=False,
- ssldir="/etc/pki/fedmsg",
-
- crl_location="https://fedoraproject.org/fedmsg/crl.pem",
- crl_cache="/tmp/crl.pem",
- crl_cache_expiry=10,
-
- certnames={
- # In prod/stg, map hostname to the name of the cert in ssldir.
- # Unfortunately, we can't use socket.getfqdn()
- #"app01.stg": "app01.stg.phx2.fedoraproject.org",
- },
-)
commit c73b4cafce03571632f966b38f8e174a80ffe04e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 3 12:42:38 2014 +0200
Bump to 2.0.2
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index dcb19ff..0af7401 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -24,7 +24,7 @@
# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
-__version__ = '2.0.1'
+__version__ = '2.0.2'
import os
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index 2daab74..f3598a7 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -1,7 +1,7 @@
%define modname fedora_elections
Name: fedora-elections
-Version: 2.0.1
+Version: 2.0.2
Release: 1%{?dist}
Summary: Fedora elections application
@@ -98,6 +98,10 @@ rm -rf $RPM_BUILD_ROOT
%changelog
+* Sat May 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.0.2-1
+- Update to 2.0.2
+- Fix is_safe_url method, imports were different here
+
* Sat May 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.0.1-1
- Update to 2.0.1
- Fix redirection after login
commit 3772fd59b6d4f12af3a7697be56e0d6b79e00e79
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 3 12:41:16 2014 +0200
Fix the is_safe_url method, the import differs here
Signed-off-by: Pierre-Yves Chibon <pingou(a)pingoured.fr>
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index d12dceb..dcb19ff 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -75,9 +75,9 @@ def is_safe_url(target):
""" Checks that the target url is safe and sending to the current
website not some other malicious one.
"""
- ref_url = urlparse.urlparse(flask.request.host_url)
- test_url = urlparse.urlparse(
- urlparse.urljoin(flask.request.host_url, target))
+ ref_url = urlparse(flask.request.host_url)
+ test_url = urlparse(
+ urljoin(flask.request.host_url, target))
return test_url.scheme in ('http', 'https') and \
ref_url.netloc == test_url.netloc
commit 2c6a551a90a1c88cf5f27bfbc01328edb92befbb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 3 12:23:31 2014 +0200
Bump to 2.0.1
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 159cc65..d12dceb 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -24,7 +24,7 @@
# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
-__version__ = '2.0'
+__version__ = '2.0.1'
import os
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index 7360ab1..2daab74 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -1,7 +1,7 @@
%define modname fedora_elections
Name: fedora-elections
-Version: 2.0
+Version: 2.0.1
Release: 1%{?dist}
Summary: Fedora elections application
@@ -98,6 +98,10 @@ rm -rf $RPM_BUILD_ROOT
%changelog
+* Sat May 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.0.1-1
+- Update to 2.0.1
+- Fix redirection after login
+
* Wed Apr 09 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.0-1
- Clean up the spec with the changes made on the setup.py
- Remove instanciated a sqlite database
commit 192f38a033ff24fcfe9f1e14845a23c38d86838f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 3 12:20:09 2014 +0200
Fix redirection bug in the login mechanism
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 58fcd09..159cc65 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -71,6 +71,17 @@ def is_authenticated():
return hasattr(flask.g, 'fas_user') and not flask.g.fas_user is None
+def is_safe_url(target):
+ """ Checks that the target url is safe and sending to the current
+ website not some other malicious one.
+ """
+ ref_url = urlparse.urlparse(flask.request.host_url)
+ test_url = urlparse.urlparse(
+ urlparse.urljoin(flask.request.host_url, target))
+ return test_url.scheme in ('http', 'https') and \
+ ref_url.netloc == test_url.netloc
+
+
def is_admin(user):
''' Is the user an elections admin.
'''
@@ -465,7 +476,8 @@ def auth_login():
return redirect.safe_redirect_back()
next_url = None
if 'next' in flask.request.args:
- next_url = flask.request.args['next']
+ if is_safe_url(flask.request.args['next']):
+ next_url = flask.request.args['next']
if not next_url or next_url == flask.url_for('.auth_login'):
next_url = flask.url_for('.index')
commit eeb1d7b9599be88d1b077f825268964639a51be0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 18:25:34 2014 +0200
Update the changelog date for 2.0 and changelog content
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index 4155beb..7360ab1 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -98,9 +98,10 @@ rm -rf $RPM_BUILD_ROOT
%changelog
-* Thu Apr 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2-2
+* Wed Apr 09 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.0-1
- Clean up the spec with the changes made on the setup.py
- Remove instanciated a sqlite database
+- Install the sql upgrade script from elections v1 to elections v2
* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 2-1
- Simple voting
commit 1b93d8ec568a8139b2dc47b219018bdc457c154b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 15:25:25 2014 +0200
Install the update_1_to_2.sql upgrade sql script to /usr/share/fedora-elections
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index 7d776ab..4155beb 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -77,6 +77,9 @@ install -m 644 files/fedora-elections.wsgi \
install -m 644 createdb.py \
$RPM_BUILD_ROOT/%{_datadir}/fedora-elections/fedora-elections_createdb.py
+install -m 644 files/update_1_to_2.sql \
+ $RPM_BUILD_ROOT/%{_datadir}/fedora-elections/update_1_to_2.sql
+
%clean
commit 533a6de95bcc6134fa65b5d5fe46d57380b285e8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 15:22:20 2014 +0200
When retrieving vote counts, only consider votes above 0
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index e38fcc6..0c55e49 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -309,6 +309,8 @@ class Vote(BASE):
cls
).filter(
cls.election_id == election_id
+ ).filter(
+ cls.value > 0
).count()
stats['n_votes'] = n_votes
@@ -328,6 +330,8 @@ class Vote(BASE):
cls.election_id == election_id
).filter(
cls.candidate_id == cand.id
+ ).filter(
+ cls.value > 0
).count()
candidate_voters[cand.name] = n_voters
commit 1edb70205643928888dd5b4988dcd1d7594dce15
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 15:10:24 2014 +0200
Little css fix to have the same padding on each side of a table cell
diff --git a/fedora_elections/static/koji.css b/fedora_elections/static/koji.css
index 2636e83..ab7331a 100644
--- a/fedora_elections/static/koji.css
+++ b/fedora_elections/static/koji.css
@@ -191,6 +191,7 @@ th {
td
{
padding-left:15px;
+ padding-right:15px;
}
th, td {
commit 3a3e3ba28cd273bf07f772c372e4fd6b1aa29f3e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:51:12 2014 +0200
Adjust the `Vote now!` links in the open elections tab
Now the `Vote now!` links are only shown if the user has not already voted
on the elections and if logged in, otherwise the links are shown
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index c71ad76..58fcd09 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -246,9 +246,18 @@ def open_elections():
flask.flash('There are no open elections.')
return redirect.safe_redirect_back()
+ voted = []
+ if is_authenticated():
+ for elec in elections:
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, elec.id, count=True)
+ if votes > 0:
+ voted.append(elec)
+
return flask.render_template(
'list/index.html',
next_elections=elections,
+ voted=voted,
tag='open',
title='Open Elections')
commit d6fad9262aabe1adfd420d7fb5813123c3d1f273
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:48:16 2014 +0200
List all the elections to the admins
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 0360c63..c71ad76 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -481,8 +481,7 @@ def auth_logout():
@APP.route('/admin/')
@election_admin_required
def admin_view_elections():
- elections = models.Election.search(
- SESSION, fas_user=flask.g.fas_user.username)
+ elections = models.Election.search(SESSION)
return flask.render_template(
'admin/all_elections.html',
commit be7947465ad23fb3e7636f87a6399e75c3f84e45
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:47:39 2014 +0200
Order the results returned by elections.search() by election date (most recent first)
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index bed6cb2..e38fcc6 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -131,6 +131,8 @@ class Election(BASE):
if fas_user is not None:
query = query.filter(cls.fas_user == fas_user)
+ query = query.order_by(sa.desc(cls.start_date))
+
return query.all()
@classmethod
commit 5b840c68cfbb3c4a6bc674ee68089f6a6ba7e79e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:34:07 2014 +0200
If the user is authenticated, only show election that the user can vote on
If the user is authenticated, retrieve the list of elections the user has
already voted on, this list is then used to only show the link `Vote now!`
on elections the user has not already voted on
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index f53bf4d..0360c63 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -179,11 +179,20 @@ def index():
cur_elections = models.Election.get_open_election(SESSION, now)
next_elections = models.Election.get_next_election(SESSION, now)[:3]
+ voted = []
+ if is_authenticated():
+ for elec in cur_elections:
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, elec.id, count=True)
+ if votes > 0:
+ voted.append(elec)
+
return flask.render_template(
'list/index.html',
prev_elections=prev_elections,
cur_elections=cur_elections,
next_elections=next_elections,
+ voted=voted,
tag='index',
title="Elections")
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 926736a..8f70756 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -22,7 +22,7 @@
<td>{{ election.status }}</td>
<td>
{% if election.status == "In progress" %}
- {% if g.fas_user %}
+ {% if election not in voted %}
{% if election.voting_type == 'range' %}
<a href="{{ url_for('vote_range', election_alias=election.alias) }}">
{% elif election.voting_type == 'simple' %}
commit 51c409c54fccecf111e86db4ccb6043cd95d1894
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:25:20 2014 +0200
Set flask.g.fas_user to None upon logout
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 0a5c7ac..f53bf4d 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -462,6 +462,7 @@ def auth_login():
def auth_logout():
if hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None:
FAS.logout()
+ flask.g.fas_user = None
flask.flash('You have been logged out')
return redirect.safe_redirect_back()
commit 8764fcf1614bff00e4609dea8b132232fd674970
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:24:47 2014 +0200
Add a dedicated is_authenticated method and use it in the decorators
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 5ce0252..0a5c7ac 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -65,6 +65,12 @@ from fedora_elections import forms
from fedora_elections import redirect
+def is_authenticated():
+ ''' Return a boolean specifying if the user is authenticated or not.
+ '''
+ return hasattr(flask.g, 'fas_user') and not flask.g.fas_user is None
+
+
def is_admin(user):
''' Is the user an elections admin.
'''
@@ -103,7 +109,7 @@ def is_election_admin(user, election_id):
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
- if not hasattr(flask.g, 'fas_user') or flask.g.fas_user is None:
+ if not is_authenticated():
return flask.redirect(flask.url_for(
'auth_login', next=flask.request.url))
return f(*args, **kwargs)
@@ -113,7 +119,7 @@ def login_required(f):
def election_admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
- if not hasattr(flask.g, 'fas_user') or flask.g.fas_user is None:
+ if not is_authenticated():
return flask.redirect(flask.url_for(
'auth_login', next=flask.request.url))
if not is_admin(flask.g.fas_user):
commit 731466cdc31d9faa6f78acac93d878ba957116bd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 09:55:33 2014 +0200
Add sql upgrade script, allowing to convert the database from elections v1 to v2
diff --git a/files/update_1_to_2.sql b/files/update_1_to_2.sql
new file mode 100644
index 0000000..99da757
--- /dev/null
+++ b/files/update_1_to_2.sql
@@ -0,0 +1,17 @@
+-- This script is used to convert an elections database from the version 0.x
+-- to the version 2.x.
+-- There was some changes made to the model during the rewrite to flask (v2.x)
+-- and this allows to convert from v1 to v2.
+
+ALTER TABLE elections ADD COLUMN voting_type VARCHAR(100) NOT NULL DEFAULT 'range';
+
+ALTER TABLE elections ADD COLUMN candidates_are_fasusers BOOLEAN NOT NULL DEFAULT false;
+
+ALTER TABLE elections ADD COLUMN fas_user VARCHAR(50);
+
+ALTER TABLE votes RENAME COLUMN weight TO value;
+
+ALTER TABLE elections DROP COLUMN votes_per_user;
+
+
+-- ALTER TABLE elections RENAME COLUMN shortdesc TO summary;
commit 57a0d99c36c1202664936d628dbeaadcbc71af06
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 09:50:10 2014 +0200
Fix ordering of the imports
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 790b698..5ce0252 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -28,20 +28,19 @@ __version__ = '2.0'
import os
+from datetime import datetime, time
+from functools import wraps
+from urlparse import urlparse, urljoin
+
import flask
-from flask.ext.fas_openid import FAS
from fedora.client import AuthError, AppError
from fedora.client.fas2 import AccountSystem
-
+from flask.ext.fas_openid import FAS
from sqlalchemy.orm.exc import NoResultFound
import fedmsgshim
-from datetime import datetime, time
-from functools import wraps
-from urlparse import urlparse, urljoin
-
APP = flask.Flask(__name__)
APP.config.from_object('fedora_elections.default_config')
if 'FEDORA_ELECTIONS_CONFIG' in os.environ:
commit cd27da7955b3052930e137500b4d2c2d0a8befd1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 09:48:57 2014 +0200
Fix import the configuration file specified by the environment variable
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 57fd4fb..790b698 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -26,6 +26,8 @@
__version__ = '2.0'
+import os
+
import flask
from flask.ext.fas_openid import FAS
@@ -42,8 +44,7 @@ from urlparse import urlparse, urljoin
APP = flask.Flask(__name__)
APP.config.from_object('fedora_elections.default_config')
-if APP.config.get('FEDORA_ELECTIONS_CONFIG') \
- and os.path.exists(APP.config['FEDORA_ELECTIONS_CONFIG']):
+if 'FEDORA_ELECTIONS_CONFIG' in os.environ:
APP.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
# set up FAS
commit 656536fbcacd0a2ddab9a5b4dd75d93b3d0162e7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 09:33:56 2014 +0200
pep8 fixes
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 0740b91..57fd4fb 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -54,7 +54,7 @@ FAS2 = AccountSystem(
APP.config['FAS_BASE_URL'],
username=APP.config['FAS_USERNAME'],
password=APP.config['FAS_PASSWORD'],
- insecure= not APP.config['FAS_CHECK_CERT']
+ insecure=not APP.config['FAS_CHECK_CERT']
)
@@ -578,7 +578,8 @@ def admin_add_candidate(election_alias):
submit_text='Add candidate')
-(a)APP.route('/admin/<election_alias>/candidates/new/multi', methods=('GET', 'POST'))
+(a)APP.route('/admin/<election_alias>/candidates/new/multi',
+ methods=('GET', 'POST'))
@election_admin_required
def admin_add_multi_candidate(election_alias):
election = models.Election.get(SESSION, alias=election_alias)
commit f41244c6b6cfe43a59b6688f67697131edea9d76
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 09:31:55 2014 +0200
When importing multiple candidates flash the number of candidates imported
It is simpler to check if all the candidates were imported correctly and
easier to read.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 3300648..0740b91 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -610,7 +610,7 @@ def admin_add_multi_candidate(election_alias):
flask.flash("There was an issue!")
SESSION.commit()
- flask.flash('Added candidates: "%s"' % ', '.join(candidates_name))
+ flask.flash('Added %s candidates' % len(candidates_name))
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
commit e738a6dc8cfcd76bafb5a6933393228ee5fa0737
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:53:53 2014 +0200
Cover case where there has been 0 votes, thus fixing the division by 0
diff --git a/fedora_elections/templates/election/results.html b/fedora_elections/templates/election/results.html
index 329f965..cc5e849 100644
--- a/fedora_elections/templates/election/results.html
+++ b/fedora_elections/templates/election/results.html
@@ -52,7 +52,11 @@
<tr>
<th>{{ candidate.name }}</th>
<td>{{ stats['candidate_voters'][candidate.name] }}</td>
+ {% if stats['candidate_voters'][candidate.name] %}
<td>{{ candidate.vote_count / stats['candidate_voters'][candidate.name] }}</td>
+ {% else %}
+ <td> - </td>
+ {% endif %}
</tr>
{% endfor %}
{% endif %}
commit f55770ae5904554f9d7e21d5844db4f0cf2daf8a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:53:16 2014 +0200
Give the total number of candidates in the view_election table
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index 5302baf..3c63681 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -18,6 +18,7 @@
</ul>
<h3>Candidates</h3>
{% if election.candidates.all() %}
+ <p>{{ election.candidates.count() }} candidates found</p>
<ul>
{% for candidate in election.candidates.all() %}
<li>{{ candidate.name }}
commit 67a7279ef1de2aaa7252b72f726e2c648ee4a340
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:53:01 2014 +0200
Fix the default title in the base master
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
index 4dc54bb..9ec43ff 100644
--- a/fedora_elections/templates/base.html
+++ b/fedora_elections/templates/base.html
@@ -3,7 +3,7 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
- <title>{% block title %}Fedora supplementary wallpapers{% endblock %}</title>
+ <title>{% block title %}Fedora elections{% endblock %}</title>
{%block head %}{% endblock %}
<link rel="shortcut icon" type="image/vnd.microsoft.icon"
href="//fedoraproject.org/static/images/favicon.ico"/>
commit 422a0f137b9c36bd51319752605234f3cba25fda
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:52:32 2014 +0200
Fix the order_by date to order in the dates in descending order
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index fbc60dc..bed6cb2 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -150,7 +150,7 @@ class Election(BASE):
).filter(
cls.end_date < limit
).order_by(
- cls.start_date
+ sa.desc(cls.start_date)
)
return query.all()
@@ -167,7 +167,7 @@ class Election(BASE):
).filter(
cls.end_date >= limit
).order_by(
- cls.start_date
+ sa.desc(cls.start_date)
)
return query.all()
@@ -182,7 +182,7 @@ class Election(BASE):
).filter(
cls.start_date > limit
).order_by(
- cls.start_date
+ sa.desc(cls.start_date)
)
return query.all()
commit 07a125c3b0d33ed59b8bf85d17e9622fc0385e37
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:50:25 2014 +0200
Change the __tablename__ from electionadmingroups to electionadmins
electionadmins is the name of the table in elections 0.x, so let's be
consistent with it instead of requiring a rename of the table on the
database
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index ec9b296..fbc60dc 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -189,7 +189,7 @@ class Election(BASE):
class ElectionAdminGroup(BASE):
- __tablename__ = 'electionadmingroups'
+ __tablename__ = 'electionadmins'
id = sa.Column(sa.Integer, primary_key=True)
election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
commit acd0939510d22f776f26d9b84353b0f7b594b823
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:48:22 2014 +0200
Remove the `role_required` field in the electionadmingroups table
This field does not exist in elections 0.x and we do not have the
information to enforce the check anyway
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index f0d3484..3300648 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -502,7 +502,7 @@ def admin_new_election():
admin = models.ElectionAdminGroup(
election=election,
group_name=APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
- role_required=u'user')
+ )
SESSION.add(admin)
SESSION.commit()
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 528ea2d..ec9b296 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -197,8 +197,6 @@ class ElectionAdminGroup(BASE):
election = relationship(
'Election', backref=backref('admin_groups', lazy='dynamic'))
group_name = sa.Column(sa.Unicode(150), nullable=False)
- role_required = sa.Column(sa.Enum(u'user', u'sponsor', u'administrator'),
- nullable=False)
@classmethod
def by_election_id(cls, session, election_id):
commit 437ab222533e35c67824af34bb33054d0a7696a1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:43:27 2014 +0200
Change the embargoed field from Boolean to Integer
The embargoed field is an Integer field in the elections 0.x database
so revert elections 2.x to the same field format
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index efb5930..f0d3484 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -487,7 +487,7 @@ def admin_new_election():
start_date=form.start_date.data,
end_date=form.end_date.data,
seats_elected=form.seats_elected.data,
- embargoed=form.embargoed.data,
+ embargoed=int(form.embargoed.data),
voting_type=form.voting_type.data,
candidates_are_fasusers=form.candidates_are_fasusers.data,
fas_user=flask.g.fas_user.username,
@@ -538,6 +538,7 @@ def admin_edit_election(election_alias):
form = forms.ElectionForm(election.id, obj=election)
if form.validate_on_submit():
+ form.embargoed.data = int(form.embargoed.data)
form.populate_obj(election)
SESSION.commit()
flask.flash('Election "%s" saved' % election.alias)
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index ccfa1b2..528ea2d 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -91,7 +91,7 @@ class Election(BASE):
start_date = sa.Column(sa.DateTime, nullable=False)
end_date = sa.Column(sa.DateTime, nullable=False)
seats_elected = sa.Column(sa.Integer, nullable=False, default=1)
- embargoed = sa.Column(sa.Boolean, nullable=False, default=True)
+ embargoed = sa.Column(sa.Integer, nullable=False, default=0)
voting_type = sa.Column(sa.Unicode(100), nullable=False, default=u'range')
candidates_are_fasusers = sa.Column(sa.Boolean, nullable=False,
default=False)
commit 7d6c1bc616c680e1d33780c211289b67b2ef00a1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:41:05 2014 +0200
Rename the number_elected back to seats_elected in the elections table
The field number_elected was named seats_elected on elections 0.x so
let's rename it back to it to avoid changes when updating.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 3b11a51..efb5930 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -486,7 +486,7 @@ def admin_new_election():
url=form.url.data,
start_date=form.start_date.data,
end_date=form.end_date.data,
- number_elected=form.number_elected.data,
+ seats_elected=form.seats_elected.data,
embargoed=form.embargoed.data,
voting_type=form.voting_type.data,
candidates_are_fasusers=form.candidates_are_fasusers.data,
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 369a663..43b228f 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -45,7 +45,7 @@ class ElectionForm(wtf.Form):
'End date<span class="error">*</span>', [
wtforms.validators.Required()])
- number_elected = wtforms.IntegerField(
+ seats_elected = wtforms.IntegerField(
'Number elected<span class="error">*</span>', [
wtforms.validators.Required(),
wtforms.validators.NumberRange(min=1)],
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index cb272e2..ccfa1b2 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -90,7 +90,7 @@ class Election(BASE):
url = sa.Column(sa.Unicode(250), nullable=False)
start_date = sa.Column(sa.DateTime, nullable=False)
end_date = sa.Column(sa.DateTime, nullable=False)
- number_elected = sa.Column(sa.Integer, nullable=False, default=1)
+ seats_elected = sa.Column(sa.Integer, nullable=False, default=1)
embargoed = sa.Column(sa.Boolean, nullable=False, default=True)
voting_type = sa.Column(sa.Unicode(100), nullable=False, default=u'range')
candidates_are_fasusers = sa.Column(sa.Boolean, nullable=False,
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index c5bf01d..95eb06b 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -18,7 +18,7 @@
{{ render_field_in_row(form.url) }}
{{ render_field_in_row(form.start_date, " at 00:00:00 UTC") }}
{{ render_field_in_row(form.end_date, " at 23:59:59 UTC") }}
- {{ render_field_in_row(form.number_elected) }}
+ {{ render_field_in_row(form.seats_elected) }}
{{ render_field_in_row(form.candidates_are_fasusers) }}
{{ render_field_in_row(form.embargoed) }}
</table>
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index fe95c6a..5302baf 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -13,7 +13,7 @@
<ul>
<li>Start date: {{ election.start_date }}</li>
<li>End date: {{ election.end_date }}</li>
- <li>Number elected: {{ election.number_elected }}</li>
+ <li>Number elected: {{ election.seats_elected }}</li>
<li>Election type: {{ election.voting_type }}</li>
</ul>
<h3>Candidates</h3>
commit 8de0d1323ee47755b2965e21515880c9537503f0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:37:59 2014 +0200
Remove the frontpage field in the elections table
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index d42f82b..3b11a51 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -488,7 +488,6 @@ def admin_new_election():
end_date=form.end_date.data,
number_elected=form.number_elected.data,
embargoed=form.embargoed.data,
- frontpage=True,
voting_type=form.voting_type.data,
candidates_are_fasusers=form.candidates_are_fasusers.data,
fas_user=flask.g.fas_user.username,
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 57a5178..cb272e2 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -92,7 +92,6 @@ class Election(BASE):
end_date = sa.Column(sa.DateTime, nullable=False)
number_elected = sa.Column(sa.Integer, nullable=False, default=1)
embargoed = sa.Column(sa.Boolean, nullable=False, default=True)
- frontpage = sa.Column(sa.Boolean, nullable=False, default=False)
voting_type = sa.Column(sa.Unicode(100), nullable=False, default=u'range')
candidates_are_fasusers = sa.Column(sa.Boolean, nullable=False,
default=False)
@@ -117,15 +116,12 @@ class Election(BASE):
return datetime.now() >= self.start_date
@classmethod
- def search(cls, session, frontpage=None, alias=None, shortdesc=None,
+ def search(cls, session, alias=None, shortdesc=None,
fas_user=None):
""" Search the election and filter based on the arguments passed.
"""
query = session.query(cls)
- if frontpage is not None:
- query = query.filter(cls.frontpage == frontpage)
-
if alias is not None:
query = query.filter(cls.alias == alias)
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index 025b13e..fe95c6a 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -11,7 +11,6 @@
</a>
</p>
<ul>
- <li>On front page: {{ election.frontpage }}</li>
<li>Start date: {{ election.start_date }}</li>
<li>End date: {{ election.end_date }}</li>
<li>Number elected: {{ election.number_elected }}</li>
commit 5ec273f16faed4ba197f8a91a28fd423f6320147
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:25:34 2014 +0200
Rename the field `election.summary` into `election.shortdesc`
shortdesc is the field used in the elections 0.x, let's keep using it to
reduce database change when updating to 2.x
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index be60fc7..d42f82b 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -480,7 +480,7 @@ def admin_new_election():
if form.validate_on_submit():
election = models.Election(
- summary=form.summary.data,
+ shortdesc=form.shortdesc.data,
alias=form.alias.data,
description=form.description.data,
url=form.url.data,
@@ -508,7 +508,7 @@ def admin_new_election():
SESSION.commit()
- flask.flash('Election "%s" added' % election.summary)
+ flask.flash('Election "%s" added' % election.alias)
fedmsgshim.publish(topic="election.new", msg=election)
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
@@ -541,7 +541,7 @@ def admin_edit_election(election_alias):
if form.validate_on_submit():
form.populate_obj(election)
SESSION.commit()
- flask.flash('Election "%s" saved' % election.summary)
+ flask.flash('Election "%s" saved' % election.alias)
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 7220374..369a663 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -6,7 +6,7 @@ from fedora_elections.models import Election
class ElectionForm(wtf.Form):
- summary = wtforms.TextField(
+ shortdesc = wtforms.TextField(
'Summary<span class="error">*</span>', [
wtforms.validators.Required(),
wtforms.validators.Length(max=150)])
@@ -61,7 +61,7 @@ class ElectionForm(wtf.Form):
form._election_id = election_id
def validate_summary(form, field):
- check = Election.search(SESSION, summary=form.summary.data)
+ check = Election.search(SESSION, shortdesc=form.shortdesc.data)
if check:
if not (form._election_id and form._election_id == check[0].id):
raise wtforms.ValidationError(
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index e482dec..57a5178 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -84,7 +84,7 @@ class Election(BASE):
__tablename__ = 'elections'
id = sa.Column(sa.Integer, primary_key=True)
- summary = sa.Column(sa.Unicode(150), unique=True, nullable=False)
+ shortdesc = sa.Column(sa.Unicode(150), unique=True, nullable=False)
alias = sa.Column(sa.Unicode(100), unique=True, nullable=False)
description = sa.Column(sa.UnicodeText, nullable=False)
url = sa.Column(sa.Unicode(250), nullable=False)
@@ -117,7 +117,7 @@ class Election(BASE):
return datetime.now() >= self.start_date
@classmethod
- def search(cls, session, frontpage=None, alias=None, summary=None,
+ def search(cls, session, frontpage=None, alias=None, shortdesc=None,
fas_user=None):
""" Search the election and filter based on the arguments passed.
"""
@@ -129,8 +129,8 @@ class Election(BASE):
if alias is not None:
query = query.filter(cls.alias == alias)
- if summary is not None:
- query = query.filter(cls.summary == summary)
+ if shortdesc is not None:
+ query = query.filter(cls.shortdesc == shortdesc)
if fas_user is not None:
query = query.filter(cls.fas_user == fas_user)
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index f27d9af..c19c831 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -27,7 +27,7 @@
<td>
<a href="{{ url_for('admin_view_election',
election_alias=election.alias) }}">
- {{ election.summary }}
+ {{ election.shortdesc }}
</a>
</td>
<td>{{ election.start_date }}</td>
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index 9488276..c5bf01d 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -11,8 +11,8 @@
<form action="" method="post">
<fieldset>
<table>
- {{ render_field_in_row(form.summary) }}
{{ render_field_in_row(form.alias) }}
+ {{ render_field_in_row(form.shortdesc) }}
{{ render_field_in_row(form.description) }}
{{ render_field_in_row(form.voting_type) }}
{{ render_field_in_row(form.url) }}
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index a36e019..025b13e 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -3,7 +3,7 @@
{%block tag %}admin{% endblock %}
{% block body %}
- <h2>{{ election.summary }}</h2>
+ <h2>{{ election.shortdesc }}</h2>
<p>{{ election.description }}</p>
<p>
<a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">
diff --git a/fedora_elections/templates/election/about.html b/fedora_elections/templates/election/about.html
index c7bf0ec..7a604bb 100644
--- a/fedora_elections/templates/election/about.html
+++ b/fedora_elections/templates/election/about.html
@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block title %}Election Information{% endblock %}
{% block body %}
- <h1>{{ election.summary }}</h1>
+ <h1>{{ election.shortdesc }}</h1>
<p>{{ election.description }}</p>
{% if election.url %}
<p><a href="{{ election.url }}">[More Information]</a></p>
diff --git a/fedora_elections/templates/election/results.html b/fedora_elections/templates/election/results.html
index 9328b1f..329f965 100644
--- a/fedora_elections/templates/election/results.html
+++ b/fedora_elections/templates/election/results.html
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block body %}
- <h1>{{ election.summary }} Results</h1>
+ <h1>{{ election.shortdesc }} Results</h1>
<p>{{ election.description }}</p>
{% if election.url %}
<p><a href="{{ election.url }}">[More Information]</a></p>
diff --git a/fedora_elections/templates/election/vote_range.html b/fedora_elections/templates/election/vote_range.html
index e3b2e84..71a7ee1 100644
--- a/fedora_elections/templates/election/vote_range.html
+++ b/fedora_elections/templates/election/vote_range.html
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block body %}
-<h2>{{election.summary}}</h2>
+<h2>{{election.shortdesc}}</h2>
<p>{{election.description}}</p>
diff --git a/fedora_elections/templates/election/vote_simple.html b/fedora_elections/templates/election/vote_simple.html
index 17434a1..0d43183 100644
--- a/fedora_elections/templates/election/vote_simple.html
+++ b/fedora_elections/templates/election/vote_simple.html
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block body %}
-<h2>{{election.summary}}</h2>
+<h2>{{election.shortdesc}}</h2>
<p>{{election.description}}</p>
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index ec0d4ca..926736a 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -16,7 +16,7 @@
</tr>
{% for election in election_list %}
<tr>
- <td>{{ election.summary }}</td>
+ <td>{{ election.shortdesc }}</td>
<td>{{ election.start_date }}</td>
<td>{{ election.end_date }}</td>
<td>{{ election.status }}</td>
commit a8e9f20ee8dc8f333f24caca4b05252c237fe185
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Apr 8 17:50:08 2014 +0200
The first release will need to be 2.0
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 3350cb4..be60fc7 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -24,7 +24,7 @@
# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
-__version__ = '0.2.0'
+__version__ = '2.0'
import flask
from flask.ext.fas_openid import FAS
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index fe10483..7d776ab 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -1,7 +1,7 @@
%define modname fedora_elections
Name: fedora-elections
-Version: 0.2
+Version: 2.0
Release: 1%{?dist}
Summary: Fedora elections application
@@ -95,12 +95,12 @@ rm -rf $RPM_BUILD_ROOT
%changelog
-* Thu Apr 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 0.2-2
+* Thu Apr 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2-2
- Clean up the spec with the changes made on the setup.py
- Remove instanciated a sqlite database
-* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.2-1
+* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 2-1
- Simple voting
-* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1-1
+* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 2-0
- Creation
commit ed08cc38293fafa04673645b7e69a86a84e74ada
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Apr 8 17:49:16 2014 +0200
Include /usr/share/fedora-elections/ in the spec file
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index e0b4461..fe10483 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -91,6 +91,7 @@ rm -rf $RPM_BUILD_ROOT
%config(noreplace) %{_sysconfdir}/httpd/conf.d/%{name}.conf
%{python_sitelib}/%{modname}/
%{python_sitelib}/%{modname}*.egg-info/
+%{_datadir}/%{name}
%changelog
commit ed0a76456479b2d5df92cd869bc2ab90ba32308c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Apr 8 17:48:50 2014 +0200
Adjust the spec file to correspond to the configuration file
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index 08c019c..e0b4461 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -65,7 +65,7 @@ install -m 644 files/fedora-elections.conf \
# Install configuration file
mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/fedora-elections
-install -m 644 files/fedora-elections.cfg.sample \
+install -m 644 files/fedora-elections.cfg \
$RPM_BUILD_ROOT/%{_sysconfdir}/fedora-elections/fedora-elections.cfg
# Install WSGI file
commit 1e19ee637b8c0924f5583417eaf12280fd17bdc2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Apr 8 17:47:21 2014 +0200
Fix the createdb.py script regarding jinja2
diff --git a/createdb.py b/createdb.py
index af15e2d..89a4a1e 100644
--- a/createdb.py
+++ b/createdb.py
@@ -1,5 +1,5 @@
import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7']
+__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
import pkg_resources
from fedora_elections import APP
commit a4bcd88723addc61ba6d554f6eb4b1342b51a2a0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon Apr 7 22:42:33 2014 +0200
The spec file has moved to the files folder
diff --git a/MANIFEST.in b/MANIFEST.in
index 5910b03..c6b69a5 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,4 @@
include AUTHORS COPYING README requirements.txt
-include fedora_electiosn.spec
include createdb.py
recursive-include fedora_elections *
recursive-include files *
commit 3a69e090d3949495e8c4f5287b646906869c6f94
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon Apr 7 10:25:12 2014 +0200
Add example configuration file explaining as well all the configuration keys
diff --git a/files/fedora-elections.cfg b/files/fedora-elections.cfg
new file mode 100644
index 0000000..0ce7c61
--- /dev/null
+++ b/files/fedora-elections.cfg
@@ -0,0 +1,28 @@
+## Example configuration file for fedora-elections
+
+
+## The secret key used by the Flask application, change it upon install
+SECRET_KEY = 'change me'
+
+
+## URL to the database server:
+#DB_URL=mysql://user:pass@host/db_name
+#DB_URL=postgres://user:pass@host/db_name
+DB_URL = 'sqlite:////var/tmp/elections_dev.sqlite'
+
+
+## One ore more FAS group that will have admin rights over the whole
+## application, including all elections past, present and future
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+
+
+## Fedora-elections can integrate with FAS to retrieve information about the
+## candidates, the following configuration keys are required for this
+## integration.
+## URL of the FAS server to use
+FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
+FAS_USERNAME = ''
+FAS_PASSWORD = ''
+## This should be ``True`` but should be changed to ``False`` if the FAS server
+## used does not have a valid SSL certificate.
+FAS_CHECK_CERT = True
commit 7658d61f780e0f0196facd3274cab88efb110958
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat Apr 5 10:31:15 2014 +0200
Simplify the .gitignore file
Most of these files are and will not in this directory
diff --git a/.gitignore b/.gitignore
index 2ff6ed6..da17908 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,13 +1,6 @@
-database.sqlite
*.pyc
*.pyo
*.swp
*.egg-info
build/
dist/
-
-createdb
-development.cfg
-fedora-elections.cfg
-fedora-elections.wsgi
-httpd-fedora-elections.conf
commit c59ba3cfdddf2a24e1da36750162827a5e1be16f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat Apr 5 10:29:22 2014 +0200
Ignore the build and dist folders from the sources
diff --git a/.gitignore b/.gitignore
index f93993b..2ff6ed6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,9 @@ database.sqlite
*.pyo
*.swp
*.egg-info
+build/
+dist/
+
createdb
development.cfg
fedora-elections.cfg
commit f0c91e9eb0e3024040773bb1e39ead7ae29de51f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Apr 4 10:28:31 2014 +0200
Update the spec file to the changes made in the setup.py and directory structure
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index 4977cb3..08c019c 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -1,7 +1,4 @@
-%{!?pyver: %define pyver %(%{__python} -c "import sys ; print sys.version[:3]")}
-
%define modname fedora_elections
-%define eggname fedora_elections
Name: fedora-elections
Version: 0.2
@@ -12,115 +9,74 @@ Group: Development/Languages
License: GPLv2+
URL: https://github.com/fedora-infra/elections
Source0: %{name}/%{name}-%{version}.tar.gz
-# Source0: https://fedorahosted.org/releases/f/a/%{name}/%{name}-%{version}.tar.gz
+#Source0: https://fedorahosted.org/releases/f/a/%{name}/%{name}-%{version}.tar.gz
BuildArch: noarch
-BuildRequires: libxslt
-BuildRequires: python
-BuildRequires: python-babel
-BuildRequires: python-backports-ssl_match_hostname
-BuildRequires: python-bunch
-BuildRequires: python-chardet
BuildRequires: python-fedora
BuildRequires: python-fedora-flask
-BuildRequires: python-flask
-BuildRequires: python-flask-sqlalchemy
BuildRequires: python-flask-wtf
-BuildRequires: python-jinja2
BuildRequires: python-kitchen
-BuildRequires: python-lxml
BuildRequires: python-openid
-BuildRequires: python-ordereddict
-BuildRequires: python-requests
+BuildRequires: python-openid-teams
+BuildRequires: python-openid-cla
BuildRequires: python-setuptools
-BuildRequires: python-simplejson
-BuildRequires: python-six
-BuildRequires: python-sqlalchemy0.7
-BuildRequires: python-urllib3
BuildRequires: python-wtforms
-Requires: libxslt
-Requires: python
-Requires: python-babel
-Requires: python-backports-ssl_match_hostname
-Requires: python-bunch
-Requires: python-chardet
Requires: python-fedora
Requires: python-fedora-flask
-Requires: python-flask
-Requires: python-flask-sqlalchemy
Requires: python-flask-wtf
-Requires: python-jinja2
Requires: python-kitchen
-Requires: python-lxml
Requires: python-openid
-Requires: python-ordereddict
-Requires: python-requests
-Requires: python-setuptools
-Requires: python-simplejson
-Requires: python-six
-Requires: python-sqlalchemy0.7
-Requires: python-urllib3
+Requires: python-openid-teams
+Requires: python-openid-cla
Requires: python-wtforms
+# EPEL6
+%if ( 0%{?rhel} && 0%{?rhel} == 6 )
+BuildRequires: python-sqlalchemy0.7
+Requires: python-sqlalchemy0.7
+%else
+BuildRequires: python-sqlalchemy > 0.5
+Requires: python-sqlalchemy > 0.5
+%endif
+
%description
fedora-elections is the Fedora Elections application.
%prep
%setup -q
-cd fedora_elections
-../configure.py
%build
-# Remove CFLAGS=... for noarch packages (unneeded)
-CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
+%{__python} setup.py build
%install
rm -rf $RPM_BUILD_ROOT
%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT
-%{__mkdir_p} %{buildroot}%{_sysconfdir}/%{name}
-%{__install} -m 444 fedora_elections/%{name}.cfg \
- %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
-
-%{__mkdir_p} %{buildroot}%{_sysconfdir}/httpd/conf.d
-%{__install} -m 444 fedora_elections/httpd-fedora-elections.conf \
- %{buildroot}%{_sysconfdir}/httpd/conf.d/httpd-fedora-elections.conf
-
-%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}
-%{__install} -m 544 fedora_elections/createdb %{buildroot}%{_datadir}/%{modname}/createdb
-%{__install} -m 544 fedora_elections/createdb.py \
- %{buildroot}%{_datadir}/%{modname}/createdb.py
-%{__install} -m 444 fedora_elections/%{name}.wsgi \
- %{buildroot}%{_datadir}/%{modname}/%{name}.wsgi
-
-%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/static
-%{__install} -m 444 fedora_elections/static/* %{buildroot}%{_datadir}/%{modname}/static
-
-%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/images
-%{__install} -m 444 fedora_elections/images/* %{buildroot}%{_datadir}/%{modname}/images
-
-%{__mkdir_p} %{buildroot}%{_sharedstatedir}/%{modname}
-
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/admin
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/auth
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/election
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/list
-%{__install} -m 444 fedora_elections/templates/*.html \
- %{buildroot}%{python_sitelib}/%{modname}/templates
-%{__install} -m 444 fedora_elections/templates/admin/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/admin
-%{__install} -m 444 fedora_elections/templates/auth/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/auth
-%{__install} -m 444 fedora_elections/templates/election/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/election
-%{__install} -m 444 fedora_elections/templates/list/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/list
+
+# Install apache configuration file
+mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/httpd/conf.d/
+install -m 644 files/fedora-elections.conf \
+ $RPM_BUILD_ROOT/%{_sysconfdir}/httpd/conf.d/fedora-elections.conf
+
+# Install configuration file
+mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/fedora-elections
+install -m 644 files/fedora-elections.cfg.sample \
+ $RPM_BUILD_ROOT/%{_sysconfdir}/fedora-elections/fedora-elections.cfg
+
+# Install WSGI file
+mkdir -p $RPM_BUILD_ROOT/%{_datadir}/fedora-elections
+install -m 644 files/fedora-elections.wsgi \
+ $RPM_BUILD_ROOT/%{_datadir}/fedora-elections/fedora-elections.wsgi
+
+# Install the createdb script
+install -m 644 createdb.py \
+ $RPM_BUILD_ROOT/%{_datadir}/fedora-elections/fedora-elections_createdb.py
+
%clean
@@ -131,51 +87,19 @@ rm -rf $RPM_BUILD_ROOT
%defattr(-,root,root,-)
%doc
%dir %{_sysconfdir}/%{name}
-%attr(744,apache,apache) %{_sharedstatedir}/%{modname}
-%{_datadir}/%{modname}/
%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
-%config(noreplace) %{_sysconfdir}/httpd/conf.d/httpd-%{name}.conf
+%config(noreplace) %{_sysconfdir}/httpd/conf.d/%{name}.conf
%{python_sitelib}/%{modname}/
-%{python_sitelib}/%{eggname}-%{version}-py%{pyver}.egg-info/
-
-
-%post
-cd %{_sharedstatedir}/%{modname}
-if [[ ! -f database.sqlite ]]
-then
- #
- # The database does not exist create one.
- #
- echo " Creating a database."
- %{_datadir}/%{modname}/createdb
- echo " Changing permissions."
- chmod 644 database.sqlite
- echo " Changing owner."
- chown apache:apache database.sqlite
-fi
-
-
-%postun
-cd %{_sharedstatedir}/%{modname}
-if [[ -f database.sqlite ]]
-then
- #
- # Remove the database.
- #
- echo " Removing the database."
- /bin/rm database.sqlite
-fi
-
-cd ${_sharedstatedir}
-if [[ -d %{_sharedstatedir}/%{modname} ]]
-then
- /bin/rmdir %{_sharedstatedir}/%{modname}
-fi
+%{python_sitelib}/%{modname}*.egg-info/
%changelog
-* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.2
+* Thu Apr 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 0.2-2
+- Clean up the spec with the changes made on the setup.py
+- Remove instanciated a sqlite database
+
+* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.2-1
- Simple voting
-* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1
+* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1-1
- Creation
commit f241ae8a05559b9a366141b8445fe28d84a7bbba
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Apr 4 10:18:47 2014 +0200
Include a MANIFEST.in files listing which files should be present in the releases
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..5910b03
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,6 @@
+include AUTHORS COPYING README requirements.txt
+include fedora_electiosn.spec
+include createdb.py
+recursive-include fedora_elections *
+recursive-include files *
+recursive-include tests *
commit 6d44730c62f9eb09a4b57ad36c20af9f8f482031
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Apr 4 10:18:41 2014 +0200
Update the setup.py script
- Update the list of dependencies
- pep8 fixes
diff --git a/setup.py b/setup.py
index b05b39b..1b3a80a 100644
--- a/setup.py
+++ b/setup.py
@@ -1,18 +1,27 @@
+#!/usr/bin/env python
+
+"""
+Setup script
+"""
+
## These two lines are needed to run on EL6
__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
import pkg_resources
from fedora_elections import __version__
-from setuptools import setup, find_packages
+from setuptools import setup
setup(
- name = 'fedora-elections',
- version = __version__,
- author = 'Frank Chiulli',
- author_email = 'fchiulli(a)fedoraproject.org',
- packages = find_packages(),
- zip_safe = False,
- include_package_data = True,
- install_requires = ['Flask', 'SQLAlchemy>=0.7', 'python-fedora'],
+ name='fedora-elections',
+ version=__version__,
+ author='Frank Chiulli',
+ author_email='fchiulli(a)fedoraproject.org',
+ packages=['fedora_elections'],
+ include_package_data=True,
+ install_requires=[
+ 'Flask', 'SQLAlchemy>=0.7', 'python-fedora', 'python-kitchen',
+ 'python-openid', 'python-openid-teams', 'python-openid-cla',
+ 'Flask-wtf', 'wtforms',
+ ],
)
commit f7c521c043be4d03c8a0444debaaf53a4e056e41
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Apr 4 10:40:38 2014 +0200
Remove configuration key that is not used anywhere
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
index 9f3a785..fcdbea0 100644
--- a/fedora_elections/default_config.py
+++ b/fedora_elections/default_config.py
@@ -12,6 +12,3 @@ FAS_USERNAME = ''
FAS_PASSWORD = ''
FAS_CHECK_CERT = False
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
-# List of URLs for additional site-wide CSS
-EXTRA_CSS = []
commit 1e77bab367e374ddd430456f28c0f963aecd3055
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:14:29 2014 +0200
Remove the `shell` script now that there is no longer development.cfg file
Since we have no longer `development.cfg` file, the shell script becomes
a simple call to either ipython or python, so there is no longer interest
in keeping it around.
diff --git a/shell b/shell
deleted file mode 100755
index fd475b5..0000000
--- a/shell
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-which ipython > /dev/null 2>&1
-if [ "$?" == "0" ]; then
- FEDORA_ELECTIONS_CONFIG=../development.cfg ipython
-else
- FEDORA_ELECTIONS_CONFIG=../development.cfg python
-fi
commit 9ef2c9ec225542d8ccb8161fb3901ee359b692c3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:13:09 2014 +0200
Rename the example wsgi file for fedora_elections
diff --git a/files/fedora-elections.wsgi b/files/fedora-elections.wsgi
new file mode 100644
index 0000000..74ca155
--- /dev/null
+++ b/files/fedora-elections.wsgi
@@ -0,0 +1,19 @@
+#-*- coding: UTF-8 -*-
+
+# These two lines are needed to run on EL6
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import sys
+sys.stdout = sys.stderr
+
+## Set the environment variable pointing to the configuration file
+#import os
+#os.environ['FEDORA_ELECTIONS_CONFIG'] = '/etc/fedora-elections/fedora-elections.cfg'
+
+## The following is only needed if you did not install
+## as a python module (for example if you run it from a git clone).
+#sys.path.insert(0, '/path/to/fedora_elections/')
+
+from fedora_elections import app as application
diff --git a/files/fedora-elections.wsgi.in b/files/fedora-elections.wsgi.in
deleted file mode 100644
index 74ca155..0000000
--- a/files/fedora-elections.wsgi.in
+++ /dev/null
@@ -1,19 +0,0 @@
-#-*- coding: UTF-8 -*-
-
-# These two lines are needed to run on EL6
-import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
-import pkg_resources
-
-import sys
-sys.stdout = sys.stderr
-
-## Set the environment variable pointing to the configuration file
-#import os
-#os.environ['FEDORA_ELECTIONS_CONFIG'] = '/etc/fedora-elections/fedora-elections.cfg'
-
-## The following is only needed if you did not install
-## as a python module (for example if you run it from a git clone).
-#sys.path.insert(0, '/path/to/fedora_elections/')
-
-from fedora_elections import app as application
commit 66f0b825dbfdd59de4346a3954c73351317fb2bf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:12:39 2014 +0200
Small adjustments to the example wsgi file
diff --git a/files/fedora-elections.wsgi.in b/files/fedora-elections.wsgi.in
index 430cc5c..74ca155 100644
--- a/files/fedora-elections.wsgi.in
+++ b/files/fedora-elections.wsgi.in
@@ -5,17 +5,15 @@ import __main__
__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
import pkg_resources
-import os
import sys
-
sys.stdout = sys.stderr
## Set the environment variable pointing to the configuration file
-os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
+#import os
+#os.environ['FEDORA_ELECTIONS_CONFIG'] = '/etc/fedora-elections/fedora-elections.cfg'
## The following is only needed if you did not install
## as a python module (for example if you run it from a git clone).
-import sys
-# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
+#sys.path.insert(0, '/path/to/fedora_elections/')
from fedora_elections import app as application
commit a08596b9d1d27020659122131295a2248255dcf1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:10:39 2014 +0200
Rename the example apache configuration file
diff --git a/files/fedora-elections.conf b/files/fedora-elections.conf
new file mode 100644
index 0000000..dc905fb
--- /dev/null
+++ b/files/fedora-elections.conf
@@ -0,0 +1,27 @@
+# Example apache configuration file for fedora-elections
+#
+# Adjust as needed, including replacing @DATADIR@ by what it should be
+
+#Alias /fedora-elections/images @DATADIR@/fedora_elections/images
+#Alias /fedora-elections/static @DATADIR@/fedora_elections/static
+#
+#WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
+#WSGISocketPrefix run/wsgi
+#WSGIRestrictStdout On
+#WSGIRestrictSignal Off
+#WSGIPythonOptimize 1
+#
+#WSGIScriptAlias /voting @DATADIR(a)/fedora_elections/fedora-elections.wsgi
+#
+#<Location />
+# WSGIProcessGroup fedora-elections
+# <IfModule mod_authz_core.c>
+# # Apache 2.4
+# Require all granted
+# </IfModule>
+# <IfModule !mod_authz_core.c>
+# # Apache 2.2
+# Order deny,allow
+# Allow from all
+# </IfModule>
+#</Location>
diff --git a/files/httpd-fedora-elections.conf.in b/files/httpd-fedora-elections.conf.in
deleted file mode 100644
index dc905fb..0000000
--- a/files/httpd-fedora-elections.conf.in
+++ /dev/null
@@ -1,27 +0,0 @@
-# Example apache configuration file for fedora-elections
-#
-# Adjust as needed, including replacing @DATADIR@ by what it should be
-
-#Alias /fedora-elections/images @DATADIR@/fedora_elections/images
-#Alias /fedora-elections/static @DATADIR@/fedora_elections/static
-#
-#WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
-#WSGISocketPrefix run/wsgi
-#WSGIRestrictStdout On
-#WSGIRestrictSignal Off
-#WSGIPythonOptimize 1
-#
-#WSGIScriptAlias /voting @DATADIR(a)/fedora_elections/fedora-elections.wsgi
-#
-#<Location />
-# WSGIProcessGroup fedora-elections
-# <IfModule mod_authz_core.c>
-# # Apache 2.4
-# Require all granted
-# </IfModule>
-# <IfModule !mod_authz_core.c>
-# # Apache 2.2
-# Order deny,allow
-# Allow from all
-# </IfModule>
-#</Location>
commit 89816f0c62682b37ac922c989d983c4a186dc1c9
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:09:51 2014 +0200
The apache configuration file should be commented out by default
We do not want to have the application running by itself after having
installed the files/rpm.
diff --git a/files/httpd-fedora-elections.conf.in b/files/httpd-fedora-elections.conf.in
index c79d2d9..dc905fb 100644
--- a/files/httpd-fedora-elections.conf.in
+++ b/files/httpd-fedora-elections.conf.in
@@ -1,23 +1,27 @@
-Alias /fedora-elections/images @DATADIR@/fedora_elections/images
-Alias /fedora-elections/static @DATADIR@/fedora_elections/static
+# Example apache configuration file for fedora-elections
+#
+# Adjust as needed, including replacing @DATADIR@ by what it should be
-WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
-WSGISocketPrefix run/wsgi
-WSGIRestrictStdout On
-WSGIRestrictSignal Off
-WSGIPythonOptimize 1
-
-WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora_elections/fedora-elections.wsgi
-
-<Location /fedora-elections>
- WSGIProcessGroup fedora-elections
- <IfModule mod_authz_core.c>
- # Apache 2.4
- Require all granted
- </IfModule>
- <IfModule !mod_authz_core.c>
- # Apache 2.2
- Order deny,allow
- Allow from all
- </IfModule>
-</Location>
+#Alias /fedora-elections/images @DATADIR@/fedora_elections/images
+#Alias /fedora-elections/static @DATADIR@/fedora_elections/static
+#
+#WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
+#WSGISocketPrefix run/wsgi
+#WSGIRestrictStdout On
+#WSGIRestrictSignal Off
+#WSGIPythonOptimize 1
+#
+#WSGIScriptAlias /voting @DATADIR(a)/fedora_elections/fedora-elections.wsgi
+#
+#<Location />
+# WSGIProcessGroup fedora-elections
+# <IfModule mod_authz_core.c>
+# # Apache 2.4
+# Require all granted
+# </IfModule>
+# <IfModule !mod_authz_core.c>
+# # Apache 2.2
+# Order deny,allow
+# Allow from all
+# </IfModule>
+#</Location>
commit 9c5db3417914a83933af52aa086cb19915f1f07a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:07:05 2014 +0200
Remove the no-longer needed configure script
diff --git a/configure.py b/configure.py
deleted file mode 100755
index 66d899f..0000000
--- a/configure.py
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/usr/bin/python -tt
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2013 Frank Chiulli <fchiulli(a)fedoraproject.org>
-#
-# This copyrighted material is made available to anyone wishing to use, modify,
-# copy, or redistribute it subject to the terms and conditions of the GNU
-# General Public License v.2, or (at your option) any later version. This
-# program is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY expressed or implied, including the implied warranties of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
-# Public License for more details. You should have received a copy of the GNU
-# General Public License along with this program; if not, write to the Free
-# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
-# code or documentation are not subject to the GNU General Public License and
-# may only be used or replicated with the express permission of Red Hat, Inc.
-#
-# Author(s): Frank Chiulli <fchiulli(a)fedoraproject.org>
-#
-
-import glob
-import os
-import re
-
-from optparse import OptionParser
-
-#
-# Process any command line options/switches.
-#
-parser = OptionParser()
-parser.add_option('--install-conf', dest='confdir', action='store',
- default='/etc',
- help='Where to install configuration files.')
-parser.add_option('--install-data', dest='datadir', action='store',
- default='/usr/share',
- help='Where to install data files.')
-parser.add_option('--shared-state', dest='sharedstdir', action='store',
- default='/var/lib',
- help='Where to install shared state files.')
-
-(options, args) = parser.parse_args()
-
-confdir = options.confdir
-datadir = options.datadir
-sharedstdir = options.sharedstdir
-
-print " confdir = %s" % confdir
-print " datadir = %s" % datadir
-print " sharedstdir = %s" % sharedstdir
-
-substs = {'@CONFDIR@':confdir,
- '@DATADIR@':datadir,
- '@SHAREDSTDIR@':sharedstdir
- }
-
-pattern = re.compile("\.in$")
-
-files = glob.glob('./*.in')
-
-#
-# Process all *.in files in the current directory.
-#
-if (files is not []):
- for orig_fn in files:
- if os.path.isfile(orig_fn):
- new_fn = re.sub(pattern, '', orig_fn)
- print " Creating %s from %s" % (new_fn, orig_fn)
- fp1 = open(orig_fn, "r")
- file_contents = fp1.read()
- fp1.close()
-
- fp2 = open(new_fn, "w")
- for key, value in substs.iteritems():
- file_contents = file_contents.replace(key, value)
- fp2.write(file_contents)
- fp2.close()
commit c937be33d30fca01e5b2f4c5e69c3d31cde702bf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:06:33 2014 +0200
Ignore .egg-info files in the git
diff --git a/.gitignore b/.gitignore
index ce498f2..f93993b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@ database.sqlite
*.pyc
*.pyo
*.swp
+*.egg-info
createdb
development.cfg
fedora-elections.cfg
commit 91cdfc5f9dcd85204c28d33cb2afce796c06ce3a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:05:59 2014 +0200
Move the spec file and the sample configuration file into the files folder
diff --git a/fedora-elections.spec b/fedora-elections.spec
deleted file mode 100644
index 4977cb3..0000000
--- a/fedora-elections.spec
+++ /dev/null
@@ -1,181 +0,0 @@
-%{!?pyver: %define pyver %(%{__python} -c "import sys ; print sys.version[:3]")}
-
-%define modname fedora_elections
-%define eggname fedora_elections
-
-Name: fedora-elections
-Version: 0.2
-Release: 1%{?dist}
-Summary: Fedora elections application
-
-Group: Development/Languages
-License: GPLv2+
-URL: https://github.com/fedora-infra/elections
-Source0: %{name}/%{name}-%{version}.tar.gz
-# Source0: https://fedorahosted.org/releases/f/a/%{name}/%{name}-%{version}.tar.gz
-
-BuildArch: noarch
-
-BuildRequires: libxslt
-BuildRequires: python
-BuildRequires: python-babel
-BuildRequires: python-backports-ssl_match_hostname
-BuildRequires: python-bunch
-BuildRequires: python-chardet
-BuildRequires: python-fedora
-BuildRequires: python-fedora-flask
-BuildRequires: python-flask
-BuildRequires: python-flask-sqlalchemy
-BuildRequires: python-flask-wtf
-BuildRequires: python-jinja2
-BuildRequires: python-kitchen
-BuildRequires: python-lxml
-BuildRequires: python-openid
-BuildRequires: python-ordereddict
-BuildRequires: python-requests
-BuildRequires: python-setuptools
-BuildRequires: python-simplejson
-BuildRequires: python-six
-BuildRequires: python-sqlalchemy0.7
-BuildRequires: python-urllib3
-BuildRequires: python-wtforms
-
-Requires: libxslt
-Requires: python
-Requires: python-babel
-Requires: python-backports-ssl_match_hostname
-Requires: python-bunch
-Requires: python-chardet
-Requires: python-fedora
-Requires: python-fedora-flask
-Requires: python-flask
-Requires: python-flask-sqlalchemy
-Requires: python-flask-wtf
-Requires: python-jinja2
-Requires: python-kitchen
-Requires: python-lxml
-Requires: python-openid
-Requires: python-ordereddict
-Requires: python-requests
-Requires: python-setuptools
-Requires: python-simplejson
-Requires: python-six
-Requires: python-sqlalchemy0.7
-Requires: python-urllib3
-Requires: python-wtforms
-
-
-%description
-fedora-elections is the Fedora Elections application.
-
-%prep
-%setup -q
-cd fedora_elections
-../configure.py
-
-
-%build
-# Remove CFLAGS=... for noarch packages (unneeded)
-CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
-
-
-%install
-rm -rf $RPM_BUILD_ROOT
-%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT
-
-%{__mkdir_p} %{buildroot}%{_sysconfdir}/%{name}
-%{__install} -m 444 fedora_elections/%{name}.cfg \
- %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
-
-%{__mkdir_p} %{buildroot}%{_sysconfdir}/httpd/conf.d
-%{__install} -m 444 fedora_elections/httpd-fedora-elections.conf \
- %{buildroot}%{_sysconfdir}/httpd/conf.d/httpd-fedora-elections.conf
-
-%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}
-%{__install} -m 544 fedora_elections/createdb %{buildroot}%{_datadir}/%{modname}/createdb
-%{__install} -m 544 fedora_elections/createdb.py \
- %{buildroot}%{_datadir}/%{modname}/createdb.py
-%{__install} -m 444 fedora_elections/%{name}.wsgi \
- %{buildroot}%{_datadir}/%{modname}/%{name}.wsgi
-
-%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/static
-%{__install} -m 444 fedora_elections/static/* %{buildroot}%{_datadir}/%{modname}/static
-
-%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/images
-%{__install} -m 444 fedora_elections/images/* %{buildroot}%{_datadir}/%{modname}/images
-
-%{__mkdir_p} %{buildroot}%{_sharedstatedir}/%{modname}
-
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/admin
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/auth
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/election
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/list
-%{__install} -m 444 fedora_elections/templates/*.html \
- %{buildroot}%{python_sitelib}/%{modname}/templates
-%{__install} -m 444 fedora_elections/templates/admin/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/admin
-%{__install} -m 444 fedora_elections/templates/auth/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/auth
-%{__install} -m 444 fedora_elections/templates/election/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/election
-%{__install} -m 444 fedora_elections/templates/list/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/list
-
-
-%clean
-rm -rf $RPM_BUILD_ROOT
-
-
-%files
-%defattr(-,root,root,-)
-%doc
-%dir %{_sysconfdir}/%{name}
-%attr(744,apache,apache) %{_sharedstatedir}/%{modname}
-%{_datadir}/%{modname}/
-%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
-%config(noreplace) %{_sysconfdir}/httpd/conf.d/httpd-%{name}.conf
-%{python_sitelib}/%{modname}/
-%{python_sitelib}/%{eggname}-%{version}-py%{pyver}.egg-info/
-
-
-%post
-cd %{_sharedstatedir}/%{modname}
-if [[ ! -f database.sqlite ]]
-then
- #
- # The database does not exist create one.
- #
- echo " Creating a database."
- %{_datadir}/%{modname}/createdb
- echo " Changing permissions."
- chmod 644 database.sqlite
- echo " Changing owner."
- chown apache:apache database.sqlite
-fi
-
-
-%postun
-cd %{_sharedstatedir}/%{modname}
-if [[ -f database.sqlite ]]
-then
- #
- # Remove the database.
- #
- echo " Removing the database."
- /bin/rm database.sqlite
-fi
-
-cd ${_sharedstatedir}
-if [[ -d %{_sharedstatedir}/%{modname} ]]
-then
- /bin/rmdir %{_sharedstatedir}/%{modname}
-fi
-
-
-%changelog
-* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.2
-- Simple voting
-
-* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1
-- Creation
diff --git a/fedora_elections/fedora-elections.wsgi.in b/fedora_elections/fedora-elections.wsgi.in
deleted file mode 100644
index 430cc5c..0000000
--- a/fedora_elections/fedora-elections.wsgi.in
+++ /dev/null
@@ -1,21 +0,0 @@
-#-*- coding: UTF-8 -*-
-
-# These two lines are needed to run on EL6
-import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
-import pkg_resources
-
-import os
-import sys
-
-sys.stdout = sys.stderr
-
-## Set the environment variable pointing to the configuration file
-os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
-
-## The following is only needed if you did not install
-## as a python module (for example if you run it from a git clone).
-import sys
-# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
-
-from fedora_elections import app as application
diff --git a/fedora_elections/httpd-fedora-elections.conf.in b/fedora_elections/httpd-fedora-elections.conf.in
deleted file mode 100644
index c79d2d9..0000000
--- a/fedora_elections/httpd-fedora-elections.conf.in
+++ /dev/null
@@ -1,23 +0,0 @@
-Alias /fedora-elections/images @DATADIR@/fedora_elections/images
-Alias /fedora-elections/static @DATADIR@/fedora_elections/static
-
-WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
-WSGISocketPrefix run/wsgi
-WSGIRestrictStdout On
-WSGIRestrictSignal Off
-WSGIPythonOptimize 1
-
-WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora_elections/fedora-elections.wsgi
-
-<Location /fedora-elections>
- WSGIProcessGroup fedora-elections
- <IfModule mod_authz_core.c>
- # Apache 2.4
- Require all granted
- </IfModule>
- <IfModule !mod_authz_core.c>
- # Apache 2.2
- Order deny,allow
- Allow from all
- </IfModule>
-</Location>
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
new file mode 100644
index 0000000..4977cb3
--- /dev/null
+++ b/files/fedora-elections.spec
@@ -0,0 +1,181 @@
+%{!?pyver: %define pyver %(%{__python} -c "import sys ; print sys.version[:3]")}
+
+%define modname fedora_elections
+%define eggname fedora_elections
+
+Name: fedora-elections
+Version: 0.2
+Release: 1%{?dist}
+Summary: Fedora elections application
+
+Group: Development/Languages
+License: GPLv2+
+URL: https://github.com/fedora-infra/elections
+Source0: %{name}/%{name}-%{version}.tar.gz
+# Source0: https://fedorahosted.org/releases/f/a/%{name}/%{name}-%{version}.tar.gz
+
+BuildArch: noarch
+
+BuildRequires: libxslt
+BuildRequires: python
+BuildRequires: python-babel
+BuildRequires: python-backports-ssl_match_hostname
+BuildRequires: python-bunch
+BuildRequires: python-chardet
+BuildRequires: python-fedora
+BuildRequires: python-fedora-flask
+BuildRequires: python-flask
+BuildRequires: python-flask-sqlalchemy
+BuildRequires: python-flask-wtf
+BuildRequires: python-jinja2
+BuildRequires: python-kitchen
+BuildRequires: python-lxml
+BuildRequires: python-openid
+BuildRequires: python-ordereddict
+BuildRequires: python-requests
+BuildRequires: python-setuptools
+BuildRequires: python-simplejson
+BuildRequires: python-six
+BuildRequires: python-sqlalchemy0.7
+BuildRequires: python-urllib3
+BuildRequires: python-wtforms
+
+Requires: libxslt
+Requires: python
+Requires: python-babel
+Requires: python-backports-ssl_match_hostname
+Requires: python-bunch
+Requires: python-chardet
+Requires: python-fedora
+Requires: python-fedora-flask
+Requires: python-flask
+Requires: python-flask-sqlalchemy
+Requires: python-flask-wtf
+Requires: python-jinja2
+Requires: python-kitchen
+Requires: python-lxml
+Requires: python-openid
+Requires: python-ordereddict
+Requires: python-requests
+Requires: python-setuptools
+Requires: python-simplejson
+Requires: python-six
+Requires: python-sqlalchemy0.7
+Requires: python-urllib3
+Requires: python-wtforms
+
+
+%description
+fedora-elections is the Fedora Elections application.
+
+%prep
+%setup -q
+cd fedora_elections
+../configure.py
+
+
+%build
+# Remove CFLAGS=... for noarch packages (unneeded)
+CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
+
+
+%install
+rm -rf $RPM_BUILD_ROOT
+%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT
+
+%{__mkdir_p} %{buildroot}%{_sysconfdir}/%{name}
+%{__install} -m 444 fedora_elections/%{name}.cfg \
+ %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
+
+%{__mkdir_p} %{buildroot}%{_sysconfdir}/httpd/conf.d
+%{__install} -m 444 fedora_elections/httpd-fedora-elections.conf \
+ %{buildroot}%{_sysconfdir}/httpd/conf.d/httpd-fedora-elections.conf
+
+%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}
+%{__install} -m 544 fedora_elections/createdb %{buildroot}%{_datadir}/%{modname}/createdb
+%{__install} -m 544 fedora_elections/createdb.py \
+ %{buildroot}%{_datadir}/%{modname}/createdb.py
+%{__install} -m 444 fedora_elections/%{name}.wsgi \
+ %{buildroot}%{_datadir}/%{modname}/%{name}.wsgi
+
+%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/static
+%{__install} -m 444 fedora_elections/static/* %{buildroot}%{_datadir}/%{modname}/static
+
+%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/images
+%{__install} -m 444 fedora_elections/images/* %{buildroot}%{_datadir}/%{modname}/images
+
+%{__mkdir_p} %{buildroot}%{_sharedstatedir}/%{modname}
+
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/admin
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/auth
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/election
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/list
+%{__install} -m 444 fedora_elections/templates/*.html \
+ %{buildroot}%{python_sitelib}/%{modname}/templates
+%{__install} -m 444 fedora_elections/templates/admin/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/admin
+%{__install} -m 444 fedora_elections/templates/auth/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/auth
+%{__install} -m 444 fedora_elections/templates/election/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/election
+%{__install} -m 444 fedora_elections/templates/list/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/list
+
+
+%clean
+rm -rf $RPM_BUILD_ROOT
+
+
+%files
+%defattr(-,root,root,-)
+%doc
+%dir %{_sysconfdir}/%{name}
+%attr(744,apache,apache) %{_sharedstatedir}/%{modname}
+%{_datadir}/%{modname}/
+%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
+%config(noreplace) %{_sysconfdir}/httpd/conf.d/httpd-%{name}.conf
+%{python_sitelib}/%{modname}/
+%{python_sitelib}/%{eggname}-%{version}-py%{pyver}.egg-info/
+
+
+%post
+cd %{_sharedstatedir}/%{modname}
+if [[ ! -f database.sqlite ]]
+then
+ #
+ # The database does not exist create one.
+ #
+ echo " Creating a database."
+ %{_datadir}/%{modname}/createdb
+ echo " Changing permissions."
+ chmod 644 database.sqlite
+ echo " Changing owner."
+ chown apache:apache database.sqlite
+fi
+
+
+%postun
+cd %{_sharedstatedir}/%{modname}
+if [[ -f database.sqlite ]]
+then
+ #
+ # Remove the database.
+ #
+ echo " Removing the database."
+ /bin/rm database.sqlite
+fi
+
+cd ${_sharedstatedir}
+if [[ -d %{_sharedstatedir}/%{modname} ]]
+then
+ /bin/rmdir %{_sharedstatedir}/%{modname}
+fi
+
+
+%changelog
+* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.2
+- Simple voting
+
+* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1
+- Creation
diff --git a/files/fedora-elections.wsgi.in b/files/fedora-elections.wsgi.in
new file mode 100644
index 0000000..430cc5c
--- /dev/null
+++ b/files/fedora-elections.wsgi.in
@@ -0,0 +1,21 @@
+#-*- coding: UTF-8 -*-
+
+# These two lines are needed to run on EL6
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import os
+import sys
+
+sys.stdout = sys.stderr
+
+## Set the environment variable pointing to the configuration file
+os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
+
+## The following is only needed if you did not install
+## as a python module (for example if you run it from a git clone).
+import sys
+# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
+
+from fedora_elections import app as application
diff --git a/files/httpd-fedora-elections.conf.in b/files/httpd-fedora-elections.conf.in
new file mode 100644
index 0000000..c79d2d9
--- /dev/null
+++ b/files/httpd-fedora-elections.conf.in
@@ -0,0 +1,23 @@
+Alias /fedora-elections/images @DATADIR@/fedora_elections/images
+Alias /fedora-elections/static @DATADIR@/fedora_elections/static
+
+WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
+WSGISocketPrefix run/wsgi
+WSGIRestrictStdout On
+WSGIRestrictSignal Off
+WSGIPythonOptimize 1
+
+WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora_elections/fedora-elections.wsgi
+
+<Location /fedora-elections>
+ WSGIProcessGroup fedora-elections
+ <IfModule mod_authz_core.c>
+ # Apache 2.4
+ Require all granted
+ </IfModule>
+ <IfModule !mod_authz_core.c>
+ # Apache 2.2
+ Order deny,allow
+ Allow from all
+ </IfModule>
+</Location>
commit b71e6e7f730a3ee83fc51f7d32947ceb28861299
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 17:07:36 2014 +0100
No need for the runserver script, elections can now run with the default configuration
diff --git a/runserver b/runserver
deleted file mode 100755
index 39898a2..0000000
--- a/runserver
+++ /dev/null
@@ -1 +0,0 @@
-FEDORA_ELECTIONS_CONFIG=../development.cfg python runserver.py
commit 0dff882a6d655e2ad757175a63d267a4086fbbdd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 17:04:48 2014 +0100
Add a generic is_election_admin checking if the user is in the admin group set for an election
Use this generic function when checking if the user is allowed to access
embargoed results.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index deecb05..3350cb4 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -82,6 +82,24 @@ def is_admin(user):
return len(set(user.groups).intersection(admins)) > 0
+def is_election_admin(user, election_id):
+ ''' Check if the provided user is in one of the admin group of the
+ specified election.
+ '''
+ if not user:
+ return False
+ if not user.cla_done or len(user.groups) < 1:
+ return False
+
+ admingroups = [
+ group.group_name
+ for group in models.ElectionAdminGroup.by_election_id(
+ SESSION, election_id=election_id)
+ ]
+
+ return len(set(user.groups).intersection(set(admingroups))) > 0
+
+
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
@@ -671,25 +689,18 @@ def election_results(election_alias):
"embargoed pending formal announcement.")
return redirect.safe_redirect_back()
else:
- if is_admin(flask.g.fas_user):
+ if is_admin(flask.g.fas_user) \
+ or is_election_admin(flask.g.fas_user, election.id):
flask.flash("The results for this election are currently "
"embargoed pending formal announcement.",
"error")
pass
else:
- match = 0
- admingroups = models.ElectionAdminGroup.by_election_id(
- SESSION, election_id=election.id)
- for admingroup in admingroups:
- if admingroup.group_name in flask.g.fas_user.groups:
- match = 1
-
- if match == 0:
- flask.flash(
- "We are sorry. The results for this election"
- "cannot be viewed because they are currently "
- "embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
+ flask.flash(
+ "We are sorry. The results for this election"
+ "cannot be viewed because they are currently "
+ "embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
usernamemap = {}
if (election.candidates_are_fasusers):
commit 9ee093bbc43a52cfef2ebcea16f7c4c3e535282f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 17:01:53 2014 +0100
Rename the is_elections_admin function to is_admin
This is more appropriate since that function relies on the admin group
set in the configuration file.
is_elections_admin should rely on the information set on the database
in relation to a specific election.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 31858a2..deecb05 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -65,7 +65,7 @@ from fedora_elections import forms
from fedora_elections import redirect
-def is_elections_admin(user):
+def is_admin(user):
''' Is the user an elections admin.
'''
if not user:
@@ -98,7 +98,7 @@ def election_admin_required(f):
if not hasattr(flask.g, 'fas_user') or flask.g.fas_user is None:
return flask.redirect(flask.url_for(
'auth_login', next=flask.request.url))
- if not is_elections_admin(flask.g.fas_user):
+ if not is_admin(flask.g.fas_user):
flask.abort(403)
return f(*args, **kwargs)
return decorated_function
@@ -141,7 +141,7 @@ def inject_variables():
user = None
if hasattr(flask.g, 'fas_user'):
user = flask.g.fas_user
- return dict(is_admin=is_elections_admin(user),
+ return dict(is_admin=is_admin(user),
version=__version__)
@@ -671,8 +671,7 @@ def election_results(election_alias):
"embargoed pending formal announcement.")
return redirect.safe_redirect_back()
else:
- if APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
- flask.g.fas_user.groups:
+ if is_admin(flask.g.fas_user):
flask.flash("The results for this election are currently "
"embargoed pending formal announcement.",
"error")
commit f59de35c56be11990b41784d16f6d80cb8a0bdcd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:34:23 2014 +0100
Clean configuration file
- Change from fakefas to stg
- Remove username
- Remove password
- Remove unused configuration key
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
index bfa6d01..9f3a785 100644
--- a/fedora_elections/default_config.py
+++ b/fedora_elections/default_config.py
@@ -7,11 +7,10 @@ DB_URL = 'sqlite:////var/tmp/elections_dev.sqlite'
# You will want to change this for your install
SECRET_KEY = 'change me'
-FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
-FAS_USERNAME = 'elections'
-FAS_PASSWORD = 'el3ctMEn0w'
+FAS_BASE_URL = 'https://admin.stg.fedoraproject.org/accounts/'
+FAS_USERNAME = ''
+FAS_PASSWORD = ''
FAS_CHECK_CERT = False
-FAS_HTTPS_REQUIRED = False
FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
# List of URLs for additional site-wide CSS
commit 878acba3aea3593e3b2150719a4522e00c9d3939
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:30:34 2014 +0100
Fix the FAS2 client insecure settings
The boolean in the configuration file specify whether to check or not
the ssl cert, but the insecure works the other way around.
If you want to check the ssl cert, you do not want to be insecure and
vice-versa.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 2a16921..31858a2 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -54,7 +54,7 @@ FAS2 = AccountSystem(
APP.config['FAS_BASE_URL'],
username=APP.config['FAS_USERNAME'],
password=APP.config['FAS_PASSWORD'],
- insecure=APP.config['FAS_CHECK_CERT']
+ insecure= not APP.config['FAS_CHECK_CERT']
)
commit 071feb321bd44677dfe113fe22bf1ee2766bd7e4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:13:33 2014 +0100
Fix the status property of an election
In the election creation form we indicate that the end_date is included
in the voting period (vote ends on end_date at 23:59:59).
This commit fixes this by checking dates instead of date/time and making
sure the end_date is included in the voting period.
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 88773ed..e482dec 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -101,10 +101,10 @@ class Election(BASE):
@property
def status(self):
now = datetime.utcnow()
- if now < self.start_date:
+ if now.date() < self.start_date.date():
return 'Pending'
else:
- if now < self.end_date:
+ if now.date() <= self.end_date.date():
return 'In progress'
else:
if self.embargoed:
commit 5ca43cc0c569acd4233494b899d994ac8771c32e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:08:57 2014 +0100
Provide in the election result page some statistics about the election
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 9d7d9e7..2a16921 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -702,7 +702,11 @@ def election_results(election_alias):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
+ stats = models.Vote.get_election_stats(SESSION, election.id)
+
return flask.render_template(
'election/results.html',
election=election,
- usernamemap=usernamemap)
+ usernamemap=usernamemap,
+ stats=stats,
+ )
diff --git a/fedora_elections/templates/election/results.html b/fedora_elections/templates/election/results.html
index f61d214..9328b1f 100644
--- a/fedora_elections/templates/election/results.html
+++ b/fedora_elections/templates/election/results.html
@@ -6,6 +6,10 @@
<p><a href="{{ election.url }}">[More Information]</a></p>
{% endif %}
<table border="1" cellpadding="1">
+ <tr>
+ <th>Candidate</th>
+ <th title="Number of votes received">Votes</th>
+ </tr>
{% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
<tr>
<td>
@@ -22,4 +26,36 @@
</tr>
{% endfor %}
</table>
+
+ <section class="stats">
+ <h3>Some statistics about this election</h3>
+ <table>
+ <tr>
+ <th>Number of voters</th>
+ <td>{{ stats['n_voters'] }}</td>
+ </tr>
+ <tr>
+ <th title="Number of votes registered, there may be more than one vote per voters">Number of votes</th>
+ <td>{{ stats['n_votes'] }}</td>
+ </tr>
+ <tr>
+ <th title="Maximum number of votes possible">Maximum of votes</th>
+ <td>{{ stats['n_voters'] * stats['n_candidates'] }}</td>
+ </tr>
+ {% if stats['candidate_voters'] %}
+ <tr>
+ <th></th>
+ <th>Number of voters per candidate</th>
+ <th>Average votes per candidate</th>
+ </tr>
+ {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
+ <tr>
+ <th>{{ candidate.name }}</th>
+ <td>{{ stats['candidate_voters'][candidate.name] }}</td>
+ <td>{{ candidate.vote_count / stats['candidate_voters'][candidate.name] }}</td>
+ </tr>
+ {% endfor %}
+ {% endif %}
+ </table>
+ </section>
{% endblock %}
commit 304b3ff19abd8805659277471f76cd6a6cbb10ac
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:08:31 2014 +0100
Add method in the Vote class to retrieve some stats on a specified elections
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index ef80530..88773ed 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -291,3 +291,51 @@ class Vote(BASE):
return query.count()
else:
return query.all()
+
+ @classmethod
+ def get_election_stats(cls, session, election_id):
+ """ Return a dictionnary containing some statistics about the
+ specified elections.
+ (Number of voters, number of votes per candidates, total number
+ of votes)
+ """
+
+ stats = {}
+
+ n_voters = session.query(
+ sa.func.distinct(cls.voter)
+ ).filter(
+ cls.election_id == election_id
+ ).count()
+ stats['n_voters'] = n_voters
+
+ n_votes = session.query(
+ cls
+ ).filter(
+ cls.election_id == election_id
+ ).count()
+ stats['n_votes'] = n_votes
+
+ election = session.query(
+ Election
+ ).filter(
+ Election.id == election_id
+ ).first()
+
+ candidate_voters = {}
+ cnt = 0
+ for cand in election.candidates:
+ cnt += 1
+ n_voters = session.query(
+ sa.func.distinct(cls.voter)
+ ).filter(
+ cls.election_id == election_id
+ ).filter(
+ cls.candidate_id == cand.id
+ ).count()
+ candidate_voters[cand.name] = n_voters
+
+ stats['candidate_voters'] = candidate_voters
+ stats['n_candidates'] = cnt
+
+ return stats
commit 2222c930c8b0b23cc37300e053f068b88374fd94
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:06:50 2014 +0100
The FAS client is FAS2 not fas2
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 844c78d..9d7d9e7 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -177,7 +177,7 @@ def about_election(election_alias):
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
+ FAS2.person_by_username(candidate.name)['human_name']
except (KeyError, AuthError):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
@@ -330,7 +330,7 @@ def vote_range(election_alias):
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
+ FAS2.person_by_username(candidate.name)['human_name']
except (KeyError, AuthError):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
@@ -401,7 +401,7 @@ def vote_simple(election_alias):
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
+ FAS2.person_by_username(candidate.name)['human_name']
except (KeyError, AuthError):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
@@ -697,7 +697,7 @@ def election_results(election_alias):
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
+ FAS2.person_by_username(candidate.name)['human_name']
except (KeyError, AuthError):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
commit e3bb36a76b640ccfe5a943f7f7c9e5cc3c7f37ae
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:05:46 2014 +0100
Small style change and add information message when displaying embargoed results
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index a0f226f..844c78d 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -664,15 +664,18 @@ def election_results(election_alias):
if not isinstance(election, models.Election):
return election
- elif election.embargoed == 1:
+ elif election.embargoed:
if not hasattr(flask.g, 'fas_user') or not flask.g.fas_user:
flask.flash("We are sorry. The results for this election"
- "cannot be viewed because they are currently"
- " embargoed pending formal announcement.")
+ "cannot be viewed because they are currently "
+ "embargoed pending formal announcement.")
return redirect.safe_redirect_back()
else:
if APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
flask.g.fas_user.groups:
+ flask.flash("The results for this election are currently "
+ "embargoed pending formal announcement.",
+ "error")
pass
else:
match = 0
@@ -685,8 +688,8 @@ def election_results(election_alias):
if match == 0:
flask.flash(
"We are sorry. The results for this election"
- "cannot be viewed because they are currently"
- " embargoed pending formal announcement.")
+ "cannot be viewed because they are currently "
+ "embargoed pending formal announcement.")
return redirect.safe_redirect_back()
usernamemap = {}
commit 5011fa7592734e338042d71c1370806b0639c815
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:04:18 2014 +0100
Mark with a red starts the field mandatory when creating an election
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 99ecf02..7220374 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -7,12 +7,12 @@ from fedora_elections.models import Election
class ElectionForm(wtf.Form):
summary = wtforms.TextField(
- 'Summary', [
+ 'Summary<span class="error">*</span>', [
wtforms.validators.Required(),
wtforms.validators.Length(max=150)])
alias = wtforms.TextField(
- 'Alias', [
+ 'Alias<span class="error">*</span>', [
wtforms.validators.Required(),
wtforms.validators.Length(max=100),
wtforms.validators.Regexp(
@@ -22,31 +22,31 @@ class ElectionForm(wtf.Form):
])
description = wtforms.TextAreaField(
- 'Description', [
+ 'Description<span class="error">*</span>', [
wtforms.validators.Required()])
voting_type = wtforms.RadioField(
- 'Type',
+ 'Type<span class="error">*</span>',
choices=[('range', 'Range Voting'),
('simple', 'Simple Voting')],
default='range')
url = wtforms.TextField(
- 'URL', [
+ 'URL<span class="error">*</span>', [
wtforms.validators.Required(),
wtforms.validators.URL(),
wtforms.validators.Length(max=250)])
start_date = wtforms.DateField(
- 'Start date', [
+ 'Start date<span class="error">*</span>', [
wtforms.validators.Required()])
end_date = wtforms.DateField(
- 'End date', [
+ 'End date<span class="error">*</span>', [
wtforms.validators.Required()])
number_elected = wtforms.IntegerField(
- 'Number elected', [
+ 'Number elected<span class="error">*</span>', [
wtforms.validators.Required(),
wtforms.validators.NumberRange(min=1)],
default=1)
commit c10145568e5f6c142f4310e16971621d62416fcb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:03:54 2014 +0100
When running elections2 locally, activate the debug mode
diff --git a/runserver.py b/runserver.py
index 293b6d0..f5ce1d9 100644
--- a/runserver.py
+++ b/runserver.py
@@ -3,4 +3,5 @@ __main__.__requires__ = ['SQLAlchemy >= 0.7']
import pkg_resources
from fedora_elections import APP
+APP.debug = True
APP.run()
commit 3351e2c4fa0a0483fdbd49c3528ad3720c7e7a67
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Apr 1 08:09:14 2014 +0200
Specify clearly that URL are optional when uploading multiple candidates
diff --git a/fedora_elections/templates/admin/candidate_multi.html b/fedora_elections/templates/admin/candidate_multi.html
index f1e4c3f..c8113a2 100644
--- a/fedora_elections/templates/admin/candidate_multi.html
+++ b/fedora_elections/templates/admin/candidate_multi.html
@@ -10,7 +10,7 @@
<p>
The string below needs to be formatted as:
- "Name1!URL1 (opt)|Name2!URL2 (opt)|Name3!URL3 (opt)"
+ "Name1!URL1 [optional]|Name2!URL2 [optional]|Name3!URL3 [optional]"
</p>
<form action="" method="post">
commit 08d67b4373e235065993e788de59d561458b4148
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Feb 20 12:04:07 2014 +0100
Add link on the admin page of an election to import multiple candidates
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index 2643297..a36e019 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -41,5 +41,9 @@
<a href="{{ url_for('admin_add_candidate',
election_alias=election.alias) }}">
Add a candidate
+ </a> -
+ <a href="{{ url_for('admin_add_multi_candidate',
+ election_alias=election.alias) }}">
+ Add a multiple candidates
</a>
{% endblock %}
commit 317f201f19b4210f71260d8f59ced3073acb050f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Feb 20 12:03:19 2014 +0100
Add the controller to import multiple candidates in one go
Fixes https://github.com/fedora-infra/elections/issues/5
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index d4c66cb..a0f226f 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -81,6 +81,7 @@ def is_elections_admin(user):
return len(set(user.groups).intersection(admins)) > 0
+
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
@@ -131,6 +132,7 @@ def get_valid_election(election_alias, ended=False):
return election
+
@APP.context_processor
def inject_variables():
''' Inject a set of variable that we want for every pages (every
@@ -558,6 +560,48 @@ def admin_add_candidate(election_alias):
submit_text='Add candidate')
+(a)APP.route('/admin/<election_alias>/candidates/new/multi', methods=('GET', 'POST'))
+@election_admin_required
+def admin_add_multi_candidate(election_alias):
+ election = models.Election.get(SESSION, alias=election_alias)
+ if not election:
+ flask.abort(404)
+
+ form = forms.MultiCandidateForm()
+ if form.validate_on_submit():
+
+ candidates_name = []
+ for entry in form.candidate.data.strip().split("|"):
+ candidate = entry.split("!")
+ #No url
+ if len(candidate) == 1:
+ cand = models.Candidate(
+ election=election,
+ name=candidate[0])
+ SESSION.add(cand)
+ candidates_name.append(cand.name)
+ # With url
+ elif len(candidate) == 2:
+ cand = models.Candidate(
+ election=election,
+ name=candidate[0],
+ url=candidate[1])
+ SESSION.add(cand)
+ candidates_name.append(cand.name)
+ else:
+ flask.flash("There was an issue!")
+
+ SESSION.commit()
+ flask.flash('Added candidates: "%s"' % ', '.join(candidates_name))
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+
+ return flask.render_template(
+ 'admin/candidate_multi.html',
+ form=form,
+ submit_text='Add candidates')
+
+
@APP.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
methods=('GET', 'POST'))
@election_admin_required
commit c8aff74aae470bc9a37280a18eb6c3360662b395
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Feb 20 12:02:47 2014 +0100
Add the template to add multiple candidates to an election
diff --git a/fedora_elections/templates/admin/candidate_multi.html b/fedora_elections/templates/admin/candidate_multi.html
new file mode 100644
index 0000000..f1e4c3f
--- /dev/null
+++ b/fedora_elections/templates/admin/candidate_multi.html
@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field_in_row %}
+
+{% block title %}{{ submit_text }}{% endblock %}
+
+{%block tag %}admin{% endblock %}
+
+{% block body %}
+ <h2>{{ submit_text }}</h2>
+
+ <p>
+ The string below needs to be formatted as:
+ "Name1!URL1 (opt)|Name2!URL2 (opt)|Name3!URL3 (opt)"
+ </p>
+
+ <form action="" method="post">
+ <fieldset>
+ <table>
+ {{ render_field_in_row(form.candidate) }}
+ </table>
+ </fieldset>
+
+ <br class="clear">
+ <p class="buttons">
+ <input type="submit" class="submit positive" value="{{ submit_text }}">
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
commit f716a70855c73b571b0366a03830623ced5017a8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Feb 20 12:02:05 2014 +0100
Add form to add multiple candidates in one go
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index b96dcc8..99ecf02 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -88,5 +88,10 @@ class CandidateForm(wtf.Form):
url = wtforms.TextField('URL', [wtforms.validators.Length(max=250)])
+class MultiCandidateForm(wtf.Form):
+ candidate = wtforms.TextField(
+ 'Candidates', [wtforms.validators.Required()])
+
+
class ConfirmationForm(wtf.Form):
pass
commit 2c573ccb6a4d659f0695bbb4f0e644e7aab0bb4a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:57:26 2014 +0100
Update the template used as front page of the admin section
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index 47e1226..f27d9af 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -17,6 +17,13 @@
{% if elections %}
<table>
<tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ </tr>
+ {% for election in elections %}
+ <tr>
<td>
<a href="{{ url_for('admin_view_election',
election_alias=election.alias) }}">
@@ -27,18 +34,6 @@
<td>{{ election.end_date }}</td>
<td>{{ election.status }}</td>
</tr>
- {% for election in elections %}
- <tr>
- <td>
- <a href="{{ url_for('admin_view_election',
- election_alias=election.alias) }}">
- {{ election.summary }}
- </a>
- </td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- </tr>
{% endfor %}
</table>
{% else %}
commit 850349497c4eebf92bc8a9e8b50f38e582633c98
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:44:37 2014 +0100
Add a little text to the front page of elections
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index d4ddc43..ec0d4ca 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -42,6 +42,19 @@
{% block body %}
<h2>{{ title }}</h2>
+ {% if tag == 'index' %}
+ <p>
+ The <a href='https://fedoraproject.org'>Fedora project</a> is a community
+ driven project. It has a number of steering commities whose responsabilities
+ is to drive the project forward.
+ </p>
+ <p>
+ These committies are elected among volunteers of the community and by
+ the community members. The present application is meant to held these
+ elections.
+ </p>
+ {% endif %}
+
{% if cur_elections %}
{{ render_elections(cur_elections, 'Current elections') }}
{% endif %}
commit 6640a24c712e01c74c30d9091d240dc46d226c81
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:37:47 2014 +0100
Remove the frontpage boolean/checkbox from the form as we no longer use it
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 26d602c..b96dcc8 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -54,8 +54,6 @@ class ElectionForm(wtf.Form):
candidates_are_fasusers = wtforms.BooleanField(
'Candidates are FAS users?')
- frontpage = wtforms.BooleanField('Show on front page?')
-
embargoed = wtforms.BooleanField('Embargo results?', default=True)
def __init__(form, election_id=None, *args, **kwargs):
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index 9a3a861..9488276 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -20,7 +20,6 @@
{{ render_field_in_row(form.end_date, " at 23:59:59 UTC") }}
{{ render_field_in_row(form.number_elected) }}
{{ render_field_in_row(form.candidates_are_fasusers) }}
- {{ render_field_in_row(form.frontpage) }}
{{ render_field_in_row(form.embargoed) }}
</table>
</fieldset>
commit 7eb4ae0fb63d2a98e0abf9eee895ed43d1bd1a6f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:37:28 2014 +0100
Small css fix to give a little space around the tables
diff --git a/fedora_elections/static/elections.css b/fedora_elections/static/elections.css
index d39dd7b..5322b11 100644
--- a/fedora_elections/static/elections.css
+++ b/fedora_elections/static/elections.css
@@ -200,3 +200,7 @@ a:visited.large_button {
.cell a {
color: #193C6E;
}
+
+body#index h3 {
+ padding-top: 1em;
+}
commit 7970b403e8eed9e9924d5e2a96bada4443454074
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:37:16 2014 +0100
Change the front page to display some information about the current, future and past elections
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 096a1ea..d4c66cb 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -147,12 +147,17 @@ def inject_variables():
@APP.route('/')
def index():
- elections = models.Election.search(SESSION, frontpage=True)
- num_elections = len(elections)
+ now = datetime.utcnow()
+
+ prev_elections = models.Election.get_older_election(SESSION, now)[:5]
+ cur_elections = models.Election.get_open_election(SESSION, now)
+ next_elections = models.Election.get_next_election(SESSION, now)[:3]
+
return flask.render_template(
'list/index.html',
- elections=elections,
- num_elections=num_elections,
+ prev_elections=prev_elections,
+ cur_elections=cur_elections,
+ next_elections=next_elections,
tag='index',
title="Elections")
@@ -181,7 +186,7 @@ def about_election(election_alias):
usernamemap=usernamemap)
-(a)APP.route('/archive')
+(a)APP.route('/archives')
def archived_elections():
now = datetime.utcnow()
@@ -208,7 +213,7 @@ def open_elections():
return flask.render_template(
'list/index.html',
- elections=elections,
+ next_elections=elections,
tag='open',
title='Open Elections')
@@ -463,7 +468,7 @@ def admin_new_election():
end_date=form.end_date.data,
number_elected=form.number_elected.data,
embargoed=form.embargoed.data,
- frontpage=form.frontpage.data,
+ frontpage=True,
voting_type=form.voting_type.data,
candidates_are_fasusers=form.candidates_are_fasusers.data,
fas_user=flask.g.fas_user.username,
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 6f9a53f..d4ddc43 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -2,9 +2,10 @@
{%block tag %}{{tag}}{% endblock %}
-{% block body %}
- <h2>{{ title }}</h2>
- {% if elections %}
+{% macro render_elections(election_list, title) -%}
+
+ <h3>{{title}}</h3>
+
<table>
<tr>
<th>Election</th>
@@ -13,7 +14,7 @@
<th>Status</th>
<th></th>
</tr>
- {% for election in elections %}
+ {% for election in election_list %}
<tr>
<td>{{ election.summary }}</td>
<td>{{ election.start_date }}</td>
@@ -35,7 +36,22 @@
</tr>
{% endfor %}
</table>
- {% else %}
- <p> No elections </p>
+
+{% endmacro %}
+
+{% block body %}
+ <h2>{{ title }}</h2>
+
+ {% if cur_elections %}
+ {{ render_elections(cur_elections, 'Current elections') }}
+ {% endif %}
+
+ {% if next_elections %}
+ {{ render_elections(next_elections, 'Next %s elections' % (next_elections|length)) }}
{% endif %}
+
+ {% if prev_elections %}
+ {{ render_elections(prev_elections, 'Last %s elections' % (prev_elections|length)) }}
+ {% endif %}
+
{% endblock %}
commit 7efe5f61e1abee15b20493ce569afd7ff5206141
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:34:57 2014 +0100
Add tags to display the menu correctly
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 4907395..096a1ea 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -153,6 +153,7 @@ def index():
'list/index.html',
elections=elections,
num_elections=num_elections,
+ tag='index',
title="Elections")
@@ -208,6 +209,7 @@ def open_elections():
return flask.render_template(
'list/index.html',
elections=elections,
+ tag='open',
title='Open Elections')
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index 4eba66f..47e1226 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -1,6 +1,8 @@
{% extends "base.html" %}
{% block title %}Election administration{% endblock %}
+{%block tag %}admin{% endblock %}
+
{% block body %}
<h2>Election administration</h2>
diff --git a/fedora_elections/templates/admin/candidate.html b/fedora_elections/templates/admin/candidate.html
index 98e0db4..06264e7 100644
--- a/fedora_elections/templates/admin/candidate.html
+++ b/fedora_elections/templates/admin/candidate.html
@@ -1,6 +1,10 @@
{% extends "base.html" %}
{% from "_formhelpers.html" import render_field_in_row %}
+
{% block title %}{{ submit_text }}{% endblock %}
+
+{%block tag %}admin{% endblock %}
+
{% block body %}
<h2>{{ submit_text }}</h2>
diff --git a/fedora_elections/templates/admin/delete_candidate.html b/fedora_elections/templates/admin/delete_candidate.html
index d764f19..26884e8 100644
--- a/fedora_elections/templates/admin/delete_candidate.html
+++ b/fedora_elections/templates/admin/delete_candidate.html
@@ -1,6 +1,10 @@
{% extends "base.html" %}
{% from "_formhelpers.html" import render_field %}
+
{% block title %}Delete candidate{% endblock %}
+
+{%block tag %}admin{% endblock %}
+
{% block body %}
<h2>{{ self.title() }}</h2>
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index a267f68..9a3a861 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -1,6 +1,10 @@
{% extends "base.html" %}
{% from "_formhelpers.html" import render_field_in_row %}
+
{% block title %}{{ submit_text }}{% endblock %}
+
+{%block tag %}admin{% endblock %}
+
{% block body %}
<h2>{{ self.title() }}</h2>
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index ed070f3..2643297 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -1,4 +1,7 @@
{% extends "base.html" %}
+
+{%block tag %}admin{% endblock %}
+
{% block body %}
<h2>{{ election.summary }}</h2>
<p>{{ election.description }}</p>
diff --git a/fedora_elections/templates/election/archive.html b/fedora_elections/templates/election/archive.html
index 9874dd5..f0d539a 100644
--- a/fedora_elections/templates/election/archive.html
+++ b/fedora_elections/templates/election/archive.html
@@ -1,4 +1,7 @@
{% extends "base.html" %}
+
+{%block tag %}archives{% endblock %}
+
{% block body %}
<h2>Past Elections</h2>
<table border="1" cellpadding="1">
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 425d70f..6f9a53f 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -1,4 +1,7 @@
{% extends "base.html" %}
+
+{%block tag %}{{tag}}{% endblock %}
+
{% block body %}
<h2>{{ title }}</h2>
{% if elections %}
commit fa5ee50cfa0b57e802b9b98ac7b281bc9a1603e3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:32:40 2014 +0100
Fix the Election.get_open_election method and add Election.get_next_election
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 113d814..ef80530 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -161,12 +161,14 @@ class Election(BASE):
@classmethod
def get_open_election(cls, session, limit):
- """ Return all the election which end_date if greater or equal to
- the provided limit.
+ """ Return all the election which start_date is lower and the
+ end_date if greater or equal to the provided limit.
"""
query = session.query(
cls
).filter(
+ cls.start_date < limit
+ ).filter(
cls.end_date >= limit
).order_by(
cls.start_date
@@ -174,6 +176,21 @@ class Election(BASE):
return query.all()
+ @classmethod
+ def get_next_election(cls, session, limit):
+ """ Return all the future elections whose start_date is greater than
+ the provided limit.
+ """
+ query = session.query(
+ cls
+ ).filter(
+ cls.start_date > limit
+ ).order_by(
+ cls.start_date
+ )
+
+ return query.all()
+
class ElectionAdminGroup(BASE):
__tablename__ = 'electionadmingroups'
commit 01bc953a73c21faeaeff3ca729fd10557bbd2711
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:57:26 2014 +0100
Update the template used as front page of the admin section
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index 1518ff8..4eba66f 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -15,10 +15,15 @@
{% if elections %}
<table>
<tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
+ <td>
+ <a href="{{ url_for('admin_view_election',
+ election_alias=election.alias) }}">
+ {{ election.summary }}
+ </a>
+ </td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
</tr>
{% for election in elections %}
<tr>
commit e86d8ef339eda7ed6667badd39216c0b76dcb171
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:18:55 2014 +0100
Rework the add election form
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index a409d2a..a267f68 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -20,67 +20,6 @@
{{ render_field_in_row(form.embargoed) }}
</table>
</fieldset>
- <div class="help">
- This is the name of the election that users will first see.</br>
- Example: <kbd>Fedora Board, November 2010</kbd>
- </div>
- <fieldset>
- {{ render_field(form.alias) }}
- </fieldset>
- <div class="help">
- A short, unique name that will be used for the election's URL.</br>
- Lowercase letters, numbers, hyphens and underscores only.</br>
- Example: <kbd>boardf15</kbd>
- </div>
- <fieldset>
- {{ render_field(form.description) }}
- </fieldset>
- <fieldset>
- <div class="row">
- {{ form.voting_type.label }}
- {% for subtype in form.voting_type %}
- {{ subtype }} {{ subtype.label }}
- {% endfor %}
- </div>
- </fieldset>
- <div class="help">
- Select the election type.
- </div>
- <fieldset>
- {{ render_field(form.url) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.start_date, " at 00:00:00 UTC") }}
- {{ render_field(form.end_date, " at 23:59:59 UTC") }}
- </fieldset>
- <div class="help">
- Enter dates in <kbd>YYYY-MM-DD</kbd> format.
- </div>
- <fieldset>
- {{ render_field(form.number_elected) }}
- </fieldset>
- <div class="help">
- This determines the cutoff for how many candidates win the election.
- For example, if you are electing four empty seats on a committee,
- enter <kbd>4</kbd> here.
- </div>
- <fieldset>
- {{ render_field(form.candidates_are_fasusers) }}
- </fieldset>
- <div class="help">
- Check this if every candidate in this election has a FAS user account.
- This will automatically pull full names and wiki page URLs from FAS.
- </div>
- <fieldset>
- {{ render_field(form.frontpage) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.embargoed) }}
- </fieldset>
- <div class="help">
- Embargo results when the election is finished. To unembargo them, edit
- the election and uncheck this box.
- </div>
<br class="clear">
<p class="buttons">
commit c2c08853f18669e8ad7b471824076fc540f78212
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:54:21 2014 +0100
Remove images we will no longer use
diff --git a/fedora_elections/images/border-left.png b/fedora_elections/images/border-left.png
deleted file mode 100644
index 6f256df..0000000
Binary files a/fedora_elections/images/border-left.png and /dev/null differ
diff --git a/fedora_elections/images/border-right.png b/fedora_elections/images/border-right.png
deleted file mode 100644
index 0e7365b..0000000
Binary files a/fedora_elections/images/border-right.png and /dev/null differ
diff --git a/fedora_elections/images/fedora-logo.png b/fedora_elections/images/fedora-logo.png
deleted file mode 100644
index ce95893..0000000
Binary files a/fedora_elections/images/fedora-logo.png and /dev/null differ
diff --git a/fedora_elections/images/line.png b/fedora_elections/images/line.png
deleted file mode 100644
index 3498cb9..0000000
Binary files a/fedora_elections/images/line.png and /dev/null differ
commit 9b4670bb847595a821236098b3dfd31a14522533
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:41:47 2014 +0100
CSS fixes: bring back the coloring when validating a vote - remove unused css
diff --git a/fedora_elections/static/elections.css b/fedora_elections/static/elections.css
index 5fcb352..d39dd7b 100644
--- a/fedora_elections/static/elections.css
+++ b/fedora_elections/static/elections.css
@@ -111,28 +111,60 @@ body#admin table {
padding: 0.6ex;
}
-#legend {
- margin-top: 4em;
- font-size: 85%;
-}
-
-#legend td {
- padding-left: 0.5em;
-}
-
-#legend td img {
- padding-right: 0.5em;
-}
-#legend th {
- min-width: 0px;
- padding-left: 0.5em;
+.vthot
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #9C0000;
+}
+
+.vtwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #FFA300;
+}
+
+.vtmedwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #93FF63;
+}
+
+.vtmedcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #00DAFE;
+}
+
+.vtcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #0033FF;
+}
+
+.vtcold
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #000083;
}
-
-#pending th {
- text-align: left;
-}
-
.italic {
font-style: italic;
}
commit a367a351acf4b210fbfc491fb0adbc1f115b2181
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:18:55 2014 +0100
Rework the add election form
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index e46bb3a..a409d2a 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -1,12 +1,24 @@
{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
+{% from "_formhelpers.html" import render_field_in_row %}
{% block title %}{{ submit_text }}{% endblock %}
{% block body %}
<h2>{{ self.title() }}</h2>
<form action="" method="post">
<fieldset>
- {{ render_field(form.summary) }}
+ <table>
+ {{ render_field_in_row(form.summary) }}
+ {{ render_field_in_row(form.alias) }}
+ {{ render_field_in_row(form.description) }}
+ {{ render_field_in_row(form.voting_type) }}
+ {{ render_field_in_row(form.url) }}
+ {{ render_field_in_row(form.start_date, " at 00:00:00 UTC") }}
+ {{ render_field_in_row(form.end_date, " at 23:59:59 UTC") }}
+ {{ render_field_in_row(form.number_elected) }}
+ {{ render_field_in_row(form.candidates_are_fasusers) }}
+ {{ render_field_in_row(form.frontpage) }}
+ {{ render_field_in_row(form.embargoed) }}
+ </table>
</fieldset>
<div class="help">
This is the name of the election that users will first see.</br>
@@ -71,10 +83,26 @@
</div>
<br class="clear">
- {% block election_form_extra %}{% endblock %}
<p class="buttons">
<input type="submit" class="submit positive" value="Save Changes">
</p>
{{ form.csrf_token }}
</form>
{% endblock %}
+
+{% block jscripts %}
+{{ super() }}
+<script type="text/javascript" src="{{ url_for('static', filename='jquery-1.10.2.min.js') }}"> </script>
+<script type="text/javascript" src="{{ url_for('static', filename='jquery-ui-1.9.2.custom.min.js') }}"> </script>
+
+<script type="text/javascript">
+ $(function(){
+ $('#start_date').datepicker({
+ dateFormat: "yy-mm-dd",
+ });
+ $('#end_date').datepicker({
+ dateFormat: "yy-mm-dd",
+ });
+ });
+</script>
+{% endblock %}
commit c557aea948cecf64fec03435a15e135425205f8d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:14:45 2014 +0100
Fix the page for adding a new candidate
diff --git a/fedora_elections/templates/admin/candidate.html b/fedora_elections/templates/admin/candidate.html
index a7b2fda..98e0db4 100644
--- a/fedora_elections/templates/admin/candidate.html
+++ b/fedora_elections/templates/admin/candidate.html
@@ -1,13 +1,15 @@
{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
+{% from "_formhelpers.html" import render_field_in_row %}
{% block title %}{{ submit_text }}{% endblock %}
{% block body %}
<h2>{{ submit_text }}</h2>
<form action="" method="post">
<fieldset>
- {{ render_field(form.name) }}
- {{ render_field(form.url) }}
+ <table>
+ {{ render_field_in_row(form.name) }}
+ {{ render_field_in_row(form.url) }}
+ </table>
</fieldset>
<br class="clear">
commit 33febfa9b30816b610d896d34d18a41741391f78
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:57:50 2014 +0100
Add dedicated macro to present the fields of a form in row
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
index ff781b7..e11eb67 100644
--- a/fedora_elections/templates/_formhelpers.html
+++ b/fedora_elections/templates/_formhelpers.html
@@ -10,4 +10,13 @@
{% endif %}
</div>
{% endmacro %}
-{# vim: set ts=2 sw=2 sta et: #}
+
+{% macro render_field_in_row(field, after="") %}
+<tr>
+ <td>{{ field.label }}</td>
+ <td>{{ field(**kwargs)|safe }}</td>
+{% if after %} <td>{{ after }}</td>{% endif %}
+{% if field.errors %}{% for error in field.errors
+%}<td class="error">{{ error }}</td>{% endfor %}{% endif %}
+</tr>
+{% endmacro %}
commit c94a845b90aaac59f2540192a7eba4ea62eb9a83
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:57:26 2014 +0100
Update the template used as front page of the admin section
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index fafefee..1518ff8 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -1,7 +1,18 @@
{% extends "base.html" %}
{% block title %}Election administration{% endblock %}
+
{% block body %}
<h2>Election administration</h2>
+
+ <ul>
+ <li>
+ <a href="{{ url_for('admin_new_election') }}">
+ Create new election
+ </a>
+ </li>
+ </ul>
+
+ {% if elections %}
<table>
<tr>
<th>Election</th>
@@ -23,4 +34,7 @@
</tr>
{% endfor %}
</table>
+ {% else %}
+ <p>No elections present in the database</p>
+ {% endif %}
{%endblock %}
commit e1a4431d24cd3030cc2f014bcccb07d84a631f3a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:57:10 2014 +0100
Inform the user if we have no election on the front page
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index b25a469..425d70f 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -1,6 +1,7 @@
{% extends "base.html" %}
{% block body %}
<h2>{{ title }}</h2>
+ {% if elections %}
<table>
<tr>
<th>Election</th>
@@ -31,4 +32,7 @@
</tr>
{% endfor %}
</table>
+ {% else %}
+ <p> No elections </p>
+ {% endif %}
{% endblock %}
commit f45807d655b88b028e0d98d444ede3ba2d0f48ab
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:56:56 2014 +0100
Update the base template to the new theme
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
index 47ab8bc..4dc54bb 100644
--- a/fedora_elections/templates/base.html
+++ b/fedora_elections/templates/base.html
@@ -1,134 +1,96 @@
<!DOCTYPE html>
-<html>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
<head>
- {% block head %}
- <title>
- Fedora Elections
- {% if self.title() %}
- —
- {% block title %}{% endblock %}
- {% endif %}
- </title>
- <link rel="stylesheet" type="text/css" media="screen"
- href="{{ url_for("static", filename="style.css") }}" />
- <link rel="stylesheet" type="text/css" media="screen"
- href="https://fedoraproject.org/static/css/fedora.css" />
- {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
- {% endfor %}
- {% endblock %}
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>{% block title %}Fedora supplementary wallpapers{% endblock %}</title>
+ {%block head %}{% endblock %}
+ <link rel="shortcut icon" type="image/vnd.microsoft.icon"
+ href="//fedoraproject.org/static/images/favicon.ico"/>
+ <link rel='stylesheet' type='text/css'
+ href="{{ url_for('static', filename='jquery-ui-1.9.2.custom.css') }}"/>
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="{{ url_for('static', filename='koji.css') }}"/>
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="{{ url_for('static', filename='elections.css') }}"/>
+ <link rel='stylesheet' type='text/css'
+ href='//fonts.googleapis.com/css?family=Comfortaa'/>
+
</head>
- <body>
- <div id="wrapper">
- <div id="head">
- <h1><a href="https://admin.fedoraproject.org/en/index">Fedora</a></h1>
- </div>
+ <body id="{%block tag %}{% endblock %}">
+
+ <div id="wrap">
+ <div id="innerwrap">
+
+ <!-- HEADER -->
+ <div id="header">
+ <img src="{{ url_for('static', filename='elections-logo.png') }}"
+ alt="Elections Logo" id="kojiLogo"/>
+ </div><!-- end header -->
- <div id="sidebar">
- <div id="nav">
- <h2>Navigation</h2>
+ <!-- MAIN NAVIGATION -->
+ <div id="mainNav">
+ <h4 class="hide">Main Site Links:</h4>
<ul>
- <li id="nav-home">
- <a href="http://fedoraproject.org/en/index">Home</a>
- </li>
- <li id="nav-get">
- <a href="http://fedoraproject.org/en/get-fedora">Get Fedora</a>
- </li>
- <li id="nav-join">
- <a href="http://fedoraproject.org/en/join-fedora">Join Fedora</a>
- </li>
- <li id="nav-help">
- <a href="http://fedoraproject.org/en/get-help">Get Help</a>
- </li>
+ <li id="indexTab"><a href="{{url_for('.index')}}">Home</a></li>
+ <li id="openTab"><a href="{{url_for('.open_elections')}}">Open</a></li>
+ <li id="archivesTab"><a href="{{url_for('.archived_elections')}}">Archives</a></li>
+ {% if is_admin %}
+ <li id="adminTab"><a href="{{url_for('.admin_view_elections')}}">Admin</a></li>
+ {% endif %}
</ul>
- </div>
- </div>
+ </div><!-- end mainNav -->
- <div id="content">
- <ul class="toolbar" id="#main-toolbar">
+ <span id="loginInfo">
{% if g.fas_user %}
- <li>
- Hello, {{ g.fas_user.username }} —
- <a href="{{ url_for('auth_logout') }}">log out</a>
- </li>
+ <span class="text">logged in as </span>
+ {{ g.fas_user.username }} |
+ <a href="{{ url_for('.auth_logout') }}">log out</a>
{% else %}
- <li>
- You are not logged in —
- <a href="{{ url_for('auth_login') }}">log in</a>
- </li>
+ <a href="{{ url_for('.auth_login') }}">login</a>
{% endif %}
- </ul>
- <h2>Elections</h2>
- <ul>
- <li>
- <a href="{{ url_for('open_elections') }}">
- Open elections
- </a>
- </li>
- <li>
- <a href="{{ url_for('archived_elections') }}">
- Archived elections
- </a>
- </li>
- </ul>
- <br>
- <h2>Administration</h2>
- <ul>
- <li>
- <a href="{{ url_for('admin_view_elections') }}">
- My elections
- </a>
- </li>
- <li>
- <a href="{{ url_for('admin_new_election') }}">
- Create new election
- </a>
- </li>
- </ul>
- {% with messages = get_flashed_messages() %}
- {% if messages %}
- <ul id="flashes">
- {% for message in messages %}
- <li>{{ message }}</li>
- {% endfor %}
- </ul>
- {% endif %}
- {% endwith %}
- {% block body %}
- {% endblock %}
- </div>
- </div>
- <div id="bottom">
- <div id="footer">
- {% block footer %}
- <p class="copy">
- Copyright © 2014 Red Hat, Inc. and others. Please send any
- comments or corrections to the
- <a href="mailto:webmaster@fedoraproject.org">
- websites team
- </a>.
- </p>
- <p class="disclaimer">
- The Fedora Project is maintained and driven by the community and
- sponsored by Red Hat. This is a community maintained site. Red
- Hat is not responsible for content.
- </p>
- <ul>
- <li class="first">
- <a href="/en/sponsors">Sponsors</a>
- </li>
- <li>
- <a href="http://fedoraproject.org/wiki/Legal">
- Legal
- </a>
- </li>
- <li>
- <a href="http://fedoraproject.org/wiki/Legal/TrademarkGuidelines">
- Trademark Guidelines
- </a>
- </li>
- </ul>
+ </span>
+
+ <div id="content">
+
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if category, messages %}
+ <ul id="flashes">
+ {% for category, message in messages %}
+ <li class="{{ category }}">{{ message |safe}}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {% endwith %}
+
+ {% block body %}{% endblock %}
+
</div>
- {% endblock %}
+
+ {% if options is defined %}
+ <div id="options">
+ <div id="options_title">
+ <h3>Options</h3>
+ </div>
+ <div id="delimiter"></div>
+ <div id="options_content">
+ {% block options %}{% endblock %}
+ </div>
+ </div>
+ {% endif %}
+
+ <p id="footer">
+ Copyright © 2013-2014 Red Hat
+ <a href="//github.com/fedora-infra/elections/"
+ target="_blank">elections</a>
+ -- version: {{ version }}
+ </p>
+
+ </div>
</div>
+
+ {% block jscripts %}
+ {% endblock %}
+
</body>
</html>
commit ef331f7e8a56de6df87ed739ee8902010fd35f14
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:55:58 2014 +0100
Add the koji.css our base and the elections.css dedicated to elections
diff --git a/fedora_elections/static/elections.css b/fedora_elections/static/elections.css
new file mode 100644
index 0000000..5fcb352
--- /dev/null
+++ b/fedora_elections/static/elections.css
@@ -0,0 +1,170 @@
+.message {
+ color: green;
+}
+
+.error {
+ color: red;
+}
+
+.warning {
+ color: #FF8000;
+}
+
+#galleria{
+ height: 250px;
+}
+
+.acls tr td {
+ vertical-align: text-top;
+}
+
+#header input {
+ border-radius: 10px;
+}
+
+h1, h2, h3 {
+ font-family: 'Comfortaa', cursive;
+}
+
+body#index #mainNav li#indexTab a,
+body#open #mainNav li#openTab a,
+body#archives #mainNav li#archivesTab a,
+body#admin #mainNav li#adminTab a {
+ background-color: #fff;
+ color: #444;
+ -webkit-border-top-left-radius: 5px;
+ -webkit-border-top-right-radius: 5px;
+ -moz-border-radius-topleft: 5px;
+ -moz-border-radius-topright: 5px;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+ min-width:0px;
+}
+
+body#admin table {
+ padding-top:2em;
+}
+
+#content {
+ float:left;
+ width:74%px;
+}
+
+#options {
+ float:right;
+ width:23%;
+ margin-right: 5px;
+ box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15) inset;
+ border-radius: 10px 10px 10px 10px;
+ position: absolute;
+ top:120px;
+ right:0;
+}
+
+#delimiter {
+ background-color:#193b6c;
+ height: 10px;
+}
+
+#options_title {
+ border-radius: 10px 10px 0px 0px;
+ padding: 1px 0em 1px 1em;
+ margin: 0px 0px 0px 0px;
+ background: #34609f;
+ color: #fff;
+}
+
+#options_content ul {
+ list-style-type: none;
+ margin-right: 5px;
+}
+
+#options_content li {
+ margin-bottom: 5px;
+}
+
+#options input[type="submit"] {
+ width: 90%;
+}
+
+/** Web App Styles **/
+/* Links to other web apps */
+#actions {
+ line-height: 1;
+ list-style: none;
+ margin: 1ex 0!important;
+ padding: 1ex 0ex 0ex 0ex;
+}
+
+#actions li {
+ display: inline;
+ margin-right: 1ex;
+}
+
+#metainfo th{
+ text-align: left;
+ background: none repeat scroll 0 0 #DDDDDD;
+}
+
+#metainfo th{
+ border: 1px solid #000000;
+ padding: 0.6ex;
+}
+
+#legend {
+ margin-top: 4em;
+ font-size: 85%;
+}
+
+#legend td {
+ padding-left: 0.5em;
+}
+
+#legend td img {
+ padding-right: 0.5em;
+}
+
+#legend th {
+ min-width: 0px;
+ padding-left: 0.5em;
+}
+
+#pending th {
+ text-align: left;
+}
+
+.italic {
+ font-style: italic;
+}
+
+.bold {
+ font-weight: bold;
+}
+
+.small {
+ font-size: 75%
+}
+
+.underline {
+ text-decoration: underline;
+}
+
+a.large_button,
+a:hover.large_button,
+a:visited.large_button {
+ color: white;
+}
+
+.large_button {
+ color: white;
+ background: #3C6EB4;
+ padding: 1em;
+ margin-bottom: 1em;
+ -moz-border-radius: 15px;
+ border-radius: 15px;
+ display:inline-block;
+}
+
+.cell a {
+ color: #193C6E;
+}
diff --git a/fedora_elections/static/header-bg.png b/fedora_elections/static/header-bg.png
new file mode 100644
index 0000000..f4c540a
Binary files /dev/null and b/fedora_elections/static/header-bg.png differ
diff --git a/fedora_elections/static/koji.css b/fedora_elections/static/koji.css
new file mode 100644
index 0000000..2636e83
--- /dev/null
+++ b/fedora_elections/static/koji.css
@@ -0,0 +1,501 @@
+/*
+ Koji styling - Updated for nuancier
+ Copyright (c) 2007,2008, 2009, 2012, 2013 Red Hat, Inc.
+
+ Authors:
+ Mike Bonnet <mikeb(a)redhat.com>
+ Mike McLean <mikem(a)redhat.com>
+ Dennis Gilmore <dgilmore(a)redhat.com>
+ Pierre-Yves Chibon <pingou(a)redhat.com>
+*/
+
+html {
+ min-width: 800px;
+}
+
+body {
+ margin: 0px;
+ padding: 0px;
+ font-size: small;
+ font-family: "Liberation Sans","Lucida Grande", "Luxi Sans", "Bitstream Vera Sans", helvetica, verdana, arial, sans-serif;
+ color: #666;
+ background: #fff url(header-bg.png) repeat-x;
+}
+
+a, a:visited, a:hover {
+ color: #0066CC;
+ text-decoration: none;
+}
+
+h4
+{
+ font-size:19px;
+ margin-bottom:14px;
+}
+
+#wrap {
+ min-width: 750px;
+ max-width:1100px;
+ margin-left:auto;
+ margin-right:auto;
+ padding: 0;
+ text-align: left;
+}
+
+#innerwrap {
+position:relative;
+}
+
+#header {
+height:87px
+}
+
+#headerSearch {
+ display:inline-block;
+ position:relative;
+ float:right;
+ top: 40px;
+}
+
+#headerSearch input,
+#headerSearch select {
+ font-size: 16px;
+}
+
+#headerSearch input[type="submit"], #pending input[type="submit"],
+.acls input[type="submit"], #options input[type="submit"]
+{
+ background-color: white;
+ box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15) inset;
+ border-radius: 10px 10px 10px 10px;
+ color: #3D69A8;
+}
+#headerSearch input[type="text"]
+{
+ border: 1px solid white;
+ box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15) inset;
+ -webkit-box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15) inset;
+ -moz-box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15) inset;
+ padding: 3px;
+}
+
+#kojiLogo {
+ /* Used only for the koji logo icon */
+ display:inline-block;
+ padding-top:8px;
+}
+
+#PoweredByKojiLogo {
+ /* Used only for the koji logo icon */
+ float: right;
+ border: 0px;
+ height: 20px;
+}
+
+
+div#content {
+ margin: 0 20px;
+ margin-top:30px;
+ clear: both;
+}
+
+p#footer {
+ padding-top: 40px;
+ margin-left: 15px;
+ line-height: 1.5em;
+ color: #999;
+ font-size: xx-small;
+ clear: both;
+}
+
+p#footer a {
+ text-decoration: none;
+}
+
+.hide {
+ display: none;
+}
+
+.disabled {
+ color: #808080;
+}
+
+#mainNav {
+ font-size:16px;
+ font-weight:bold;
+ height: 30px;
+}
+
+#mainNav ul {
+ padding: 0px;
+ margin: 0px;
+ list-style-type: none;
+}
+
+#mainNav ul li {
+
+ float: left;
+
+}
+
+#mainNav ul li a {
+ display: block;
+ color: rgba(255,255,255,0.8);
+ text-decoration: none;
+ padding: 0.5em 1.5em;
+ font-size: 0.77em;
+ height: 1.5em;
+}
+
+#mainNav ul li:hover {
+ -webkit-border-top-left-radius: 5px;
+ -webkit-border-top-right-radius: 5px;
+ -moz-border-radius-topleft: 5px;
+ -moz-border-radius-topright: 5px;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+ background-color:rgba(255,255,255,0.1);
+}
+
+body#summary #mainNav li#summaryTab a,
+body#tasks #mainNav li#tasksTab a,
+body#tags #mainNav li#tagsTab a,
+body#builds #mainNav li#buildsTab a,
+body#packages #mainNav li#packagesTab a,
+body#users #mainNav li#usersTab a,
+body#hosts #mainNav li#hostsTab a,
+body#buildtargets #mainNav li#buildtargetsTab a,
+body#reports #mainNav li#reportsTab a,
+body#search #mainNav li#searchTab a {
+ background-color: #fff;
+ color: #444;
+ -webkit-border-top-left-radius: 5px;
+ -webkit-border-top-right-radius: 5px;
+ -moz-border-radius-topleft: 5px;
+ -moz-border-radius-topright: 5px;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+}
+
+table {
+ border-spacing: 0px;
+}
+
+th {
+ font-weight: bold;
+ vertical-align: text-top;
+ /*min-width: 100px;*/
+ /*text-align:right;*/
+}
+
+td
+{
+ padding-left:15px;
+}
+
+th, td {
+ line-height:19px;
+}
+
+td.building {
+ color: #cc0;
+ width:45px;
+}
+
+td.complete {
+ color: #0c0;
+ width:45px;
+}
+
+td.deleted,
+td.failed,
+td.canceled {
+ color: #c00;
+ width:45px;
+}
+
+td.false {
+ color: #c00;
+}
+
+td.true {
+ color: #0c0;
+}
+
+img.sort {
+ /* used for up/down sort arrows*/
+ vertical-align: baseline;
+ width: 10px;
+ height: 9px;
+}
+
+td.paginate {
+ text-align: center;
+}
+
+form.pageJump {
+ float: right;
+ margin-left: 20px;
+}
+
+form.pageJump select {
+ font-size: smaller;
+}
+
+div.dataHeader {
+ font-weight: bold;
+ font-size:17px;
+ line-height:28px;
+ padding-bottom:4px;
+}
+
+div.pageHeader {
+ margin-bottom: 10px;
+ font-weight: bold;
+ font-size: 19px;
+}
+
+table.nested {
+ float: left;
+}
+
+td.container {
+ /*padding: 4px 0px;*/
+ width: 100%;
+}
+
+table.nested th,
+table.nested td {
+ padding: 2px 4px;
+}
+
+div.toggle {
+ padding: 6px;
+}
+
+td.tree {
+
+}
+
+.tree span.root {
+ font-weight: bold;
+
+}
+
+.tree ul {
+ padding-left: 2em;
+ list-style: none;
+ margin-top: 0em;
+ margin-bottom: 0em;
+}
+
+.tree span.treeBranch {
+ border-bottom: 1px solid #000;
+ border-left: 1px solid #000;
+ font-size: 1.2em;
+}
+
+.tree li.sibling > span.treeBranch {
+ border-left-width: 0em;
+}
+
+.tree li.sibling {
+ border-left: 1px solid #000;
+}
+
+.tree a {
+ text-decoration: none;
+}
+
+.tree span.treeLabel {
+ position: relative;
+ top: 0.6em;
+ margin-left: 1.2em;
+ padding-left: 0.2em;
+
+ font-size: 0.83em;
+}
+
+.tree > ul {
+ padding-bottom: 0.6em;
+}
+
+.hidden {
+ display: none;
+}
+
+.tree span.treeToggle {
+ font-weight: bold;
+}
+
+.tree span.treeLink {
+ font-size: smaller;
+}
+
+.adminLink {
+ color: #000;
+}
+
+img.stateimg {
+ margin-top: -6px;
+ margin-bottom: -6px;
+}
+
+.charlist {
+ text-align: center;
+}
+
+img.graphrow {
+ background-color: #00f;
+ vertical-align: bottom;
+}
+
+table.data-list {
+ width: 100%;
+}
+
+table.data-list td {
+ vertical-align: text-top;
+ padding-left:3px;
+}
+
+tr.list-header {
+ background-color: #fff;
+}
+
+tr.list-header th {
+ background-color: #eee;
+ box-shadow: 0 -1px 0 #ddd, 0 1px 0 #ddd;
+ text-align:left;
+ padding-left:4px;
+}
+
+tr.list-header th {
+min-width:1px;
+}
+
+table.nested th
+{
+ min-width:1px;
+}
+
+tr.row-even,
+tr.row-odd
+{
+ box-shadow: 0 1px 0 #ddd;
+}
+
+tr.row-even td,
+tr.row-odd td
+{
+padding-left:4px;
+}
+
+tr.row-odd td:first-child,
+tr.row-even td:first-child
+ {
+ box-shadow: -1px 0 0 #ddd;
+}
+
+tr.row-odd td:last-child,
+tr.row-even td:last-child {
+ box-shadow: 1px 0 0 #ddd;
+}
+
+tr.list-header th:first-child
+{
+ box-shadow: -1px 0 0 #ddd,0 -1px 0 #ddd, 0 1px 0 #ddd;
+}
+
+tr.list-header th:last-child
+{
+ box-shadow: 1px 0 0 #ddd,0 -1px 0 #ddd, 0 1px 0 #ddd;
+}
+
+tr.row-even td.tree {
+
+}
+
+tr.row-even td.tree span.treeLabel {
+
+}
+
+.taskfree, .taskfree:visited, .taskfree:hover {
+ color: #3300CC;
+ background-color:#fff;
+}
+.taskopen, .taskopen:visited, .taskopen:hover {
+ color: #FF6600;
+ background-color:#fff;
+}
+.taskclosed, .taskclosed:visited, .taskclosed:hover {
+ color: #00CC00;
+ background-color:#fff;
+}
+.taskcanceled, .taskcanceled:visited, .taskcanceled:hover {
+ color: #CC9900;
+ background-color:#fff;
+}
+.taskassigned, .taskassigned:visited, .taskassigned:hover {
+ color: #CC00FF;
+ background-color:#fff;
+}
+.taskfailed, .taskfailed:visited, .taskfailed:hover {
+ color: #CC0000;
+ background-color:#fff;
+}
+
+a.help {
+ text-decoration: underline;
+}
+
+abbr {
+ cursor: help;
+}
+
+.changelog {
+ font-family: monospace;
+ font-size: medium;
+ white-space: pre;
+}
+
+#headerHelp {
+ float: right;
+ margin: 15px 10px 0 0;
+}
+
+.filterlist {
+ font-size: smaller;
+}
+
+span#loginInfo {
+ background-color: #ccc;
+ font-weight: bold;
+ padding: 3px 15px;
+ position: absolute;
+ right: 0;
+ top: 0;
+ -webkit-border-bottom-left-radius: 3px;
+ -webkit-border-bottom-right-radius: 3px;
+ -moz-border-radius-bottomleft: 3px;
+ -moz-border-radius-bottomright: 3px;
+ border-bottom-left-radius: 3px;
+ border-bottom-right-radius: 3px;
+ box-shadow: 0 10px 10px rgba(255, 255, 255, 0.5) inset, 0 1px 3px rgba(0, 0, 0, 0.5);
+ -mox-box-shadow: 0 10px 10px rgba(255, 255, 255, 0.5) inset, 0 1px 3px rgba(0, 0, 0, 0.5);
+ -webkit-box-shadow: 0 10px 10px rgba(255, 255, 255, 0.5) inset, 0 1px 3px rgba(0, 0, 0, 0.5);
+}
+
+.smaller {
+ font-size: smaller;
+}
+
+.rpmheader {
+ /*font-family: monospace;
+ font-size: medium;
+ white-space: pre;*/
+}
+
+.error {
+ color: red;
+}
commit c711f8a1da50343a4daf7787eafc14e98b4040f2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:55:34 2014 +0100
Add jQuery CSS
diff --git a/fedora_elections/static/jquery-ui-1.9.2.custom.css b/fedora_elections/static/jquery-ui-1.9.2.custom.css
new file mode 100644
index 0000000..93bc89f
--- /dev/null
+++ b/fedora_elections/static/jquery-ui-1.9.2.custom.css
@@ -0,0 +1,416 @@
+/*! jQuery UI - v1.9.2 - 2012-11-26
+* http://jqueryui.com
+* Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.accordion.css, jquery.ui.button.css, jquery.ui.datepicker.css, jquery.ui.dialog.css, jquery.ui.menu.css, jquery.ui.spinner.css
+* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Helvetica%2CArial%2Csans-serif...
Highlight=55&borderColorHighlight=cccccc&fcHighlight=444444&iconColorHighlight=3c6eb4&bgColorError=ffffff&bgTextureError=01_flat.png&bgImgOpacityError=55&borderColorError=db3279&fcError=222222&iconColorError=db3279&bgColorOv%20%20%20%20erlay=eeeeee&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=80&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=60&thicknessShadow=4px&offsetTopShadow=-4px&offsetLeftShadow=-4px&c%20%20%20%20ornerRadiusShadow=0px
+* Copyright (c) 2012 jQuery Foundation and other contributors Licensed MIT */
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden { display: none; }
+.ui-helper-hidden-accessible { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
+.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
+.ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; }
+.ui-helper-clearfix:after { clear: both; }
+.ui-helper-clearfix { zoom: 1; }
+.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled { cursor: default !important; }
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
+.ui-resizable { position: relative;}
+.ui-resizable-handle { position: absolute;font-size: 0.1px; display: block; }
+.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
+.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
+.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
+.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }
+.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }
+.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
+.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
+.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
+.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}.ui-accordion .ui-accordion-header { display: block; cursor: pointer; position: relative; margin-top: 2px; padding: .5em .5em .5em .7em; zoom: 1; }
+.ui-accordion .ui-accordion-icons { padding-left: 2.2em; }
+.ui-accordion .ui-accordion-noicons { padding-left: .7em; }
+.ui-accordion .ui-accordion-icons .ui-accordion-icons { padding-left: 2.2em; }
+.ui-accordion .ui-accordion-header .ui-accordion-header-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
+.ui-accordion .ui-accordion-content { /*padding: 1em 2.2em; such a big padding cause issues on fedocal menu */ border-top: 0; overflow: auto; zoom: 1; }
+.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */
+.ui-button, .ui-button:link, .ui-button:visited, .ui-button:hover, .ui-button:active { text-decoration: none; }
+.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
+button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
+.ui-button-icons-only { width: 3.4em; }
+button.ui-button-icons-only { width: 3.7em; }
+
+/*button text element */
+.ui-button .ui-button-text { display: block; line-height: 1.4; }
+.ui-button-text-only .ui-button-text { padding: .4em 1em; }
+.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; }
+.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; }
+.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; }
+.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
+/* no icon support for input elements, provide padding by default */
+input.ui-button { padding: .4em 1em; }
+
+/*button icon element(s) */
+.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; }
+.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; }
+.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; }
+.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
+.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
+
+/*button sets*/
+.ui-buttonset { margin-right: 7px; }
+.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; }
+
+/* workarounds */
+button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
+.ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; }
+.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
+.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
+.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
+.ui-datepicker .ui-datepicker-prev { left:2px; }
+.ui-datepicker .ui-datepicker-next { right:2px; }
+.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
+.ui-datepicker .ui-datepicker-next-hover { right:1px; }
+.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; }
+.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
+.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; }
+.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
+.ui-datepicker select.ui-datepicker-month,
+.ui-datepicker select.ui-datepicker-year { width: 49%;}
+.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
+.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; }
+.ui-datepicker td { border: 0; padding: 1px; }
+.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
+.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; }
+.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi { width:auto; }
+.ui-datepicker-multi .ui-datepicker-group { float:left; }
+.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
+.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
+.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
+.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
+.ui-datepicker-row-break { clear:both; width:100%; font-size:0em; }
+
+/* RTL support */
+.ui-datepicker-rtl { direction: rtl; }
+.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+
+/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
+.ui-datepicker-cover {
+ position: absolute; /*must have*/
+ z-index: -1; /*must have*/
+ filter: mask(); /*must have*/
+ top: -4px; /*must have*/
+ left: -4px; /*must have*/
+ width: 200px; /*must have*/
+ height: 200px; /*must have*/
+}.ui-dialog { position: absolute; top: 0; left: 0; padding: .2em; width: 300px; overflow: hidden; }
+.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; }
+.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; }
+.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
+.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
+.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
+.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
+.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
+.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; }
+.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; }
+.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
+.ui-draggable .ui-dialog-titlebar { cursor: move; }
+.ui-menu { list-style:none; padding: 2px; margin: 0; display:block; outline: none; }
+.ui-menu .ui-menu { margin-top: -3px; position: absolute; }
+.ui-menu .ui-menu-item { margin: 0; padding: 0; zoom: 1; width: 100%; }
+.ui-menu .ui-menu-divider { margin: 5px -2px 5px -2px; height: 0; font-size: 0; line-height: 0; border-width: 1px 0 0 0; }
+.ui-menu .ui-menu-item a { text-decoration: none; display: block; padding: 2px .4em; line-height: 1.5; zoom: 1; font-weight: normal; }
+.ui-menu .ui-menu-item a.ui-state-focus,
+.ui-menu .ui-menu-item a.ui-state-active { font-weight: normal; margin: -1px; }
+
+.ui-menu .ui-state-disabled { font-weight: normal; margin: .4em 0 .2em; line-height: 1.5; }
+.ui-menu .ui-state-disabled a { cursor: default; }
+
+/* icon support */
+.ui-menu-icons { position: relative; }
+.ui-menu-icons .ui-menu-item a { position: relative; padding-left: 2em; }
+
+/* left-aligned */
+.ui-menu .ui-icon { position: absolute; top: .2em; left: .2em; }
+
+/* right-aligned */
+.ui-menu .ui-menu-icon { position: static; float: right; }
+.ui-spinner { position:relative; display: inline-block; overflow: hidden; padding: 0; vertical-align: middle; }
+.ui-spinner-input { border: none; background: none; padding: 0; margin: .2em 0; vertical-align: middle; margin-left: .4em; margin-right: 22px; }
+.ui-spinner-button { width: 16px; height: 50%; font-size: .5em; padding: 0; margin: 0; text-align: center; position: absolute; cursor: default; display: block; overflow: hidden; right: 0; }
+.ui-spinner a.ui-spinner-button { border-top: none; border-bottom: none; border-right: none; } /* more specificity required here to overide default borders */
+.ui-spinner .ui-icon { position: absolute; margin-top: -8px; top: 50%; left: 0; } /* vertical centre icon */
+.ui-spinner-up { top: 0; }
+.ui-spinner-down { bottom: 0; }
+
+/* TR overrides */
+.ui-spinner .ui-icon-triangle-1-s {
+ /* need to fix icons sprite */
+ background-position:-65px -16px;
+}
+
+/* Component containers
+----------------------------------*/
+.ui-widget { font-family: Helvetica,Arial,sans-serif; font-size: 1.1em; }
+.ui-widget .ui-widget { font-size: 1em; }
+.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Helvetica,Arial,sans-serif; font-size: 1em; }
+.ui-widget-content { border: 1px solid #dddddd; background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x; color: #444444; }
+/*.ui-widget-content a { color: #444444; }*/
+.ui-widget-header { border: 1px solid #dddddd; background: #dddddd url(images/ui-bg_highlight-soft_75_dddddd_1x100.png) 50% 50% repeat-x; color: #444444; font-weight: bold; }
+.ui-widget-header a { color: #444444; }
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #dddddd; background: #f6f6f6 url(images/ui-bg_highlight-soft_100_f6f6f6_1x100.png) 50% 50% repeat-x; font-weight: bold; color: #3c6eb4; }
+.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #3c6eb4; text-decoration: none; }
+.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #3c6eb 4; background: #3c6eb4 url(images/ui-bg_highlight-soft_25_3c6eb4_1x100.png) 50% 50% repeat-x; font-weight: bold; color: #ffffff; }
+.ui-state-hover a, .ui-state-hover a:hover, .ui-state-hover a:link, .ui-state-hover a:visited { color: #ffffff; text-decoration: none; }
+.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #dddddd; background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #e59728; }
+.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #e59728; text-decoration: none; }
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #cccccc; background: #ffffff url(images/ui-bg_flat_55_ffffff_40x100.png) 50% 50% repeat-x; color: #444444; }
+.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #444444; }
+.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #db3279; background: #ffffff url(images/ui-bg_flat_55_ffffff_40x100.png) 50% 50% repeat-x; color: #222222; }
+.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #222222; }
+.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #222222; }
+.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
+.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
+.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
+.ui-state-disabled .ui-icon { filter:Alpha(Opacity=35); } /* For IE8 - See #6059 */
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_db3279_256x240.png); }
+.ui-widget-content .ui-icon {background-image: url(images/ui-icons_db3279_256x240.png); }
+.ui-widget-header .ui-icon {background-image: url(images/ui-icons_3c6eb4_256x240.png); }
+.ui-state-default .ui-icon { background-image: url(images/ui-icons_666666_256x240.png); }
+.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); }
+.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); }
+.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_3c6eb4_256x240.png); }
+.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_db3279_256x240.png); }
+
+/* positioning */
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-on { background-position: -96px -144px; }
+.ui-icon-radio-off { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-start { background-position: -80px -160px; }
+/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 2px; -webkit-border-top-left-radius: 2px; -khtml-border-top-left-radius: 2px; border-top-left-radius: 2px; }
+.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 2px; -webkit-border-top-right-radius: 2px; -khtml-border-top-right-radius: 2px; border-top-right-radius: 2px; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 2px; -webkit-border-bottom-left-radius: 2px; -khtml-border-bottom-left-radius: 2px; border-bottom-left-radius: 2px; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 2px; -webkit-border-bottom-right-radius: 2px; -khtml-border-bottom-right-radius: 2px; border-bottom-right-radius: 2px; }
+
+/* Overlays */
+.ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .8;filter:Alpha(Opacity=80); }
+.ui-widget-shadow { margin: -4px 0 0 -4px; padding: 4px; background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .6;filter:Alpha(Opacity=60); -moz-border-radius: 8px; -khtml-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }
commit cce9fd261d148eb2ad265d98006d6eeaf3d1592f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:55:25 2014 +0100
Add jQuery and our jQuery UI
diff --git a/fedora_elections/static/jquery-1.10.2.min.js b/fedora_elections/static/jquery-1.10.2.min.js
new file mode 100644
index 0000000..da41706
--- /dev/null
+++ b/fedora_elections/static/jquery-1.10.2.min.js
@@ -0,0 +1,6 @@
+/*! jQuery v1.10.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license
+//@ sourceMappingURL=jquery-1.10.2.min.map
+*/
+(function(e,t){var n,r,i=typeof t,o=e.location,a=e.document,s=a.documentElement,l=e.jQuery,u=e.$,c={},p=[],f="1.10.2",d=p.concat,h=p.push,g=p.slice,m=p.indexOf,y=c.toString,v=c.hasOwnProperty,b=f.trim,x=function(e,t){return new x.fn.init(e,t,r)},w=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=/\S+/g,C=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)retu
rn this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:fun
ction(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expa
ndo:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)
for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){
t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];e
lse while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a
={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=st(),k=st(),E=st
(),S=!1,A=function(e,t){return e===t?(S=!0,0):0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp
("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.n
odeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=mt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+yt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,n,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function lt(e){return e[b]=!0,e}function ut(e)
{var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ct(e,t){var n=e.split("|"),r=e.length;while(r--)o.attrHandle[n[r]]=t}function pt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function dt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return lt(function(t){return t=+t,lt(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.defaultView;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.docume
ntElement,h=!s(n),i&&i.attachEvent&&i!==i.top&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),r.getElementsByTagName=ut(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ut(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ut(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TA
G=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=K.test(n.querySelectorAll))&&(ut(function(e){e.innerHTML="<select><option selected=''></option></select>",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ut(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=K.test(y=d.webkitMatchesSelector||d.mozM
atchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ut(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=K.test(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c
,e)-F.call(c,t):0;if(o===a)return pt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?pt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDupl
icates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:lt,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2]
;return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=mt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!
!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?lt(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call
(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:lt(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?lt(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:lt(function(e){return function(t){return at(e,t).length>0}}),contains:lt(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:lt(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:funct
ion(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for
(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},o.pseudos.nth=o.pseudos.eq;for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=ft(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=dt(n);function gt(){}gt.prototype=o.filters=o.pseudos,o.setFilters=new gt;function mt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function yt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function vt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;r
eturn t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function bt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function wt(e,t,n,r,i,o){return r&&!r[b]&&(r=wt(r)),i&&!i[b]&&(i=wt(i,o)),lt(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||Nt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:xt(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=xt(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y
=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=xt(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function Tt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=vt(function(e){return e===t},s,!0),p=vt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[vt(bt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return wt(l>1&&bt(f),l>1&&yt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&Tt(e.slice(l,r)),i>r&&Tt(e=e.slice(r)),i>r&&yt(e))}f.push(n)}return bt(f)}function Ct(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b])
;b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=xt(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?lt(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=mt(e)),n=t.length;while(n--)o=Tt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Ct(i,r))}return o};function Nt(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function kt(e,t,n,i){var a,s,u,c,p,f=mt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parent
Node||t))){if(s.splice(a,1),e=i.length&&yt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}r.sortStable=b.split("").sort(A).join("")===b,r.detectDuplicates=S,p(),r.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(f.createElement("div"))}),ut(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||ct("type|href|height|width",function(e,n,r){return r?t:e.getAttribute(n,"type"===n.toLowerCase()?1:2)}),r.attributes&&ut(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ct("value",function(e,n,r){return r||"input"!==e.nodeName.toLowerCase()?t:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||ct(B,function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&i.specified?i.value:e[n]===!0?n.toLowerCase():null}),x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXML
Doc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,
r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!l||i&&!u||(t=t||[],t=[e,t.slice?t.slice():t],n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o
){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement
("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav></:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("
value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;
left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="<table><tr><td></td><td>t</td></tr></table>",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),ty
peof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="<div></div>",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null)}),n=s=l=u=r=o=null,t
+}({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{ap
plet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(functi
on(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x
.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.acc
ess(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.clas
sName+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,r=0,o=x(this),a=e.match(T)||[];while(t=a[r++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this
).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0
)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFi
x[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocu
ment.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxL
ength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,a
rguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.
*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n
]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this
,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt
.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.
clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEv
entListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t)
{x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(th
is,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0==
=n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.
event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;f
or(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling"
)},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i
},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/<tbody/i,wt=/<|&#?\w+;/,Tt=/<(?:script|style|link)/i,Ct=/^(?:checkbox|radio)$/i,Nt=/checked\s*(?:[^=]|=\s*.checked.)/i,kt=/^$|
\/(?:java|ecma)script/i,Et=/^true\/(.*)/,St=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,At={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:x.support.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prep
end:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.acc
ess(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1></$2>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i
=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._dat
a(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"repl
aceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a
++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1></$2>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?"<table>"!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-
1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle);
+u[o]&&(delete u[o],c?delete n[l]:typeof n.removeAttribute!==i?n.removeAttribute(l):n[l]=null,p.push(o))}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}}),x.fn.extend({wrapAll:function(e){if(x.isFunction(e))return this.each(function(t){x(this).wrapAll(e.call(this,t))});if(this[0]){var t=x(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var P
t,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+w+")(.*)$","i"),Yt=RegExp("^("+w+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+w+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=x._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=x._data(r,"olddisplay",ln(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&x._data(r,"olddisplay",i?n:x.css(r,"display"))));for(a=0;s>a;a+
+)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}x.fn.extend({css:function(e,n){return x.access(this,function(e,n,r){var i,o,a={},s=0;if(x.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=x.css(e,n[s],!1,o);return a}return r!==t?x.style(e,n,r):x.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){nn(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":x.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,l=x.camelCase(n),u=e.style;if(n=x.cssProps[l]||(x.cssProps[l]=tn
(u,l)),s=x.cssHooks[n]||x.cssHooks[l],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:u[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(x.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||x.cssNumber[l]||(r+="px"),x.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(u[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{u[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,l=x.camelCase(n);return n=x.cssProps[l]||(x.cssProps[l]=tn(e.style,l)),s=x.cssHooks[n]||x.cssHooks[l],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||x.isNumeric(o)?o||0:a):a}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s.getPropertyValue(n)||s[n]:t,u=e.style;return s&&(""!==l||x.contains(e.ownerDocument,e)||(l=x.style(e,n)),Yt.test(l)&&Ut.test(n)&&(i=u.width,o=u.minWidth,a=u.maxWidth,u.minWidth=u.max
Width=u.width=l,l=s.width,u.width=i,u.minWidth=o,u.maxWidth=a)),l}):a.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s[n]:t,u=e.style;return null==l&&u&&u[n]&&(l=u[n]),Yt.test(l)&&!zt.test(n)&&(i=u.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),u.left="fontSize"===n?"1em":l,l=u.pixelLeft+"px",u.left=i,a&&(o.left=a)),""===l?"auto":l});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=x.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=x.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=x.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=x.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=x.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,
o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function ln(e){var t=a,n=Gt[e];return n||(n=un(e,t),"none"!==n&&n||(Pt=(Pt||x("<iframe frameborder='0' width='0' height='0'/>").css("cssText","display:block !important")).appendTo(t.documentElement),t=(Pt[0].contentWindow||Pt[0].contentDocument).document,t.write("<!doctype html><html><body>"),t.close(),n=un(e,t),Pt.detach()),Gt[e]=n),n}function un(e,t){var n=x(t.createElement(e)).appendTo(t.body),r=x.css(n[0],"display");return n.remove(),r}x.each(["height","width"],function(e,n){x.cssHooks[n]={get:function(e,r,i){return r?0===e.offsetWidth&&Xt.test(x.css(e,"display"))?x.swap(e,Qt,function(){return sn(e,n,i)}):sn(e,n,i):t},set:function(e,t,r){var i=r&&Rt(e);return on(e,t,r?an(e,n,r,x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,i),i):0)}}}),x.support.opacity|
|(x.cssHooks.opacity={get:function(e,t){return It.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=x.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===x.trim(o.replace($t,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=$t.test(o)?o.replace($t,i):o+" "+i)}}),x(function(){x.support.reliableMarginRight||(x.cssHooks.marginRight={get:function(e,n){return n?x.swap(e,{display:"inline-block"},Wt,[e,"marginRight"]):t}}),!x.support.pixelPosition&&x.fn.position&&x.each(["top","left"],function(e,n){x.cssHooks[n]={get:function(e,r){return r?(r=Wt(e,n),Yt.test(r)?x(e).position()[n]+"px":r):t}}})}),x.expr&&x.expr.filters&&(x.expr.filters.hidden=function(e){return 0>=e.offsetWidth&&0>=e.offsetHeight||!x.support.reliableHiddenOffsets&&"none"===(e.style&&e.style.display||x.css(e,"display"))},x.expr
.filters.visible=function(e){return!x.expr.filters.hidden(e)}),x.each({margin:"",padding:"",border:"Width"},function(e,t){x.cssHooks[e+t]={expand:function(n){var r=0,i={},o="string"==typeof n?n.split(" "):[n];for(;4>r;r++)i[e+Zt[r]+t]=o[r]||o[r-2]||o[0];return i}},Ut.test(e)||(x.cssHooks[e+t].set=on)});var cn=/%20/g,pn=/\[\]$/,fn=/\r?\n/g,dn=/^(?:submit|button|image|reset|file)$/i,hn=/^(?:input|select|textarea|keygen)/i;x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=x.prop(this,"elements");return e?x.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!x(this).is(":disabled")&&hn.test(this.nodeName)&&!dn.test(e)&&(this.checked||!Ct.test(e))}).map(function(e,t){var n=x(this).val();return null==n?null:x.isArray(n)?x.map(n,function(e){return{name:t.name,value:e.replace(fn,"\r\n")}}):{name:t.name,value:n.replace(fn,"\r\n")}}).get()}}),x.param=function(e,n){var r,i=[],o=functi
on(e,t){t=x.isFunction(t)?t():null==t?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(n===t&&(n=x.ajaxSettings&&x.ajaxSettings.traditional),x.isArray(e)||e.jquery&&!x.isPlainObject(e))x.each(e,function(){o(this.name,this.value)});else for(r in e)gn(r,e[r],n,o);return i.join("&").replace(cn,"+")};function gn(e,t,n,r){var i;if(x.isArray(t))x.each(t,function(t,i){n||pn.test(e)?r(e,i):gn(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==x.type(t))r(e,t);else for(i in t)gn(e+"["+i+"]",t[i],n,r)}x.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){x.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),x.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:fu
nction(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var mn,yn,vn=x.now(),bn=/\?/,xn=/#.*$/,wn=/([?&])_=[^&]*/,Tn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Cn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Nn=/^(?:GET|HEAD)$/,kn=/^\/\//,En=/^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,Sn=x.fn.load,An={},jn={},Dn="*/".concat("*");try{yn=o.href}catch(Ln){yn=a.createElement("a"),yn.href="",yn=yn.href}mn=En.exec(yn.toLowerCase())||[];function Hn(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(T)||[];if(x.isFunction(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function qn(e,n,r,i){var o={},a=e===jn;function s(l){var u;return o[l]=!0,x.each(e[l]||[],function(e,l){var c=l(n,r,i);return"string"!=typeof c||a||o[c]?a?!(u=c):t:(n.dataTypes.unshift(c),s(c
),!1)}),u}return s(n.dataTypes[0])||!o["*"]&&s("*")}function _n(e,n){var r,i,o=x.ajaxSettings.flatOptions||{};for(i in n)n[i]!==t&&((o[i]?e:r||(r={}))[i]=n[i]);return r&&x.extend(!0,e,r),e}x.fn.load=function(e,n,r){if("string"!=typeof e&&Sn)return Sn.apply(this,arguments);var i,o,a,s=this,l=e.indexOf(" ");return l>=0&&(i=e.slice(l,e.length),e=e.slice(0,l)),x.isFunction(n)?(r=n,n=t):n&&"object"==typeof n&&(a="POST"),s.length>0&&x.ajax({url:e,type:a,dataType:"html",data:n}).done(function(e){o=arguments,s.html(i?x("<div>").append(x.parseHTML(e)).find(i):e)}).complete(r&&function(e,t){s.each(r,o||[e.responseText,t,e])}),this},x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){x.fn[t]=function(e){return this.on(t,e)}}),x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:yn,type:"GET",isLocal:Cn.test(mn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Dn,text:"t
ext/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":x.parseJSON,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?_n(_n(e,x.ajaxSettings),t):_n(x.ajaxSettings,e)},ajaxPrefilter:Hn(An),ajaxTransport:Hn(jn),ajax:function(e,n){"object"==typeof e&&(n=e,e=t),n=n||{};var r,i,o,a,s,l,u,c,p=x.ajaxSetup({},n),f=p.context||p,d=p.context&&(f.nodeType||f.jquery)?x(f):x.event,h=x.Deferred(),g=x.Callbacks("once memory"),m=p.statusCode||{},y={},v={},b=0,w="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c){c={};while(t=Tn.exec(a))c[t[1].toLowerCase()]=t[2]}t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase(
);return b||(e=v[n]=v[n]||e,y[e]=t),this},overrideMimeType:function(e){return b||(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)m[t]=[m[t],e[t]];else C.always(e[C.status]);return this},abort:function(e){var t=e||w;return u&&u.abort(t),k(0,t),this}};if(h.promise(C).complete=g.add,C.success=C.done,C.error=C.fail,p.url=((e||p.url||yn)+"").replace(xn,"").replace(kn,mn[1]+"//"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=x.trim(p.dataType||"*").toLowerCase().match(T)||[""],null==p.crossDomain&&(r=En.exec(p.url.toLowerCase()),p.crossDomain=!(!r||r[1]===mn[1]&&r[2]===mn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(mn[3]||("http:"===mn[1]?"80":"443")))),p.data&&p.processData&&"string"!=typeof p.data&&(p.data=x.param(p.data,p.traditional)),qn(An,p,n,C),2===b)return C;l=p.global,l&&0===x.active++&&x.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Nn.test(p.type),o=p.url,p.hasContent||(p.data&&(o=p.url+=(bn.test(o)?"&":"?")+p.data,
delete p.data),p.cache===!1&&(p.url=wn.test(o)?o.replace(wn,"$1_="+vn++):o+(bn.test(o)?"&":"?")+"_="+vn++)),p.ifModified&&(x.lastModified[o]&&C.setRequestHeader("If-Modified-Since",x.lastModified[o]),x.etag[o]&&C.setRequestHeader("If-None-Match",x.etag[o])),(p.data&&p.hasContent&&p.contentType!==!1||n.contentType)&&C.setRequestHeader("Content-Type",p.contentType),C.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Dn+"; q=0.01":""):p.accepts["*"]);for(i in p.headers)C.setRequestHeader(i,p.headers[i]);if(p.beforeSend&&(p.beforeSend.call(f,C,p)===!1||2===b))return C.abort();w="abort";for(i in{success:1,error:1,complete:1})C[i](p[i]);if(u=qn(jn,p,n,C)){C.readyState=1,l&&d.trigger("ajaxSend",[C,p]),p.async&&p.timeout>0&&(s=setTimeout(function(){C.abort("timeout")},p.timeout));try{b=1,u.send(y,k)}catch(N){if(!(2>b))throw N;k(-1,N)}}else k(-1,"No Transport");function k(e,n,r,i){var c,y,v,w,T,N=n;2!==b&&(b=2,s&&
clearTimeout(s),u=t,a=i||"",C.readyState=e>0?4:0,c=e>=200&&300>e||304===e,r&&(w=Mn(p,C,r)),w=On(p,w,C,c),c?(p.ifModified&&(T=C.getResponseHeader("Last-Modified"),T&&(x.lastModified[o]=T),T=C.getResponseHeader("etag"),T&&(x.etag[o]=T)),204===e||"HEAD"===p.type?N="nocontent":304===e?N="notmodified":(N=w.state,y=w.data,v=w.error,c=!v)):(v=N,(e||!N)&&(N="error",0>e&&(e=0))),C.status=e,C.statusText=(n||N)+"",c?h.resolveWith(f,[y,N,C]):h.rejectWith(f,[C,N,v]),C.statusCode(m),m=t,l&&d.trigger(c?"ajaxSuccess":"ajaxError",[C,p,c?y:v]),g.fireWith(f,[C,N]),l&&(d.trigger("ajaxComplete",[C,p]),--x.active||x.event.trigger("ajaxStop")))}return C},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,n){return x.get(e,t,n,"script")}}),x.each(["get","post"],function(e,n){x[n]=function(e,r,i,o){return x.isFunction(r)&&(o=o||i,i=r,r=t),x.ajax({url:e,type:n,dataType:o,data:r,success:i})}});function Mn(e,n,r){var i,o,a,s,l=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),o=
==t&&(o=e.mimeType||n.getResponseHeader("Content-Type"));if(o)for(s in l)if(l[s]&&l[s].test(o)){u.unshift(s);break}if(u[0]in r)a=u[0];else{for(s in r){if(!u[0]||e.converters[s+" "+u[0]]){a=s;break}i||(i=s)}a=a||i}return a?(a!==u[0]&&u.unshift(a),r[a]):t}function On(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(p){return{state:"parsererror",error:a?p:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-
ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),x.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=a.head||x("head")[0]||a.documentElement;return{send:function(t,i){n=a.createElement("script"),n.async=!0,e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,t){(t||!n.readyState||/loaded|complete/.test(n.readyState))&&(n.onload=n.onreadystatechange=null,n.parentNode&&n.parentNode.removeChild(n),n=null,t||i(200,"success"))},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(t,!0)}}}});var Fn=[],Bn=/(=)\?(?=&|$)|\?\?/;x.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Fn.pop()||x.expando+"_"+vn++;return this[e]=!0,e}}),x.ajaxPrefilter("json jsonp",function(n,r,i){var o,a,s,l=n.jsonp!==!1&&(Bn.test(n.url)?"url":"string"==typeof
n.data&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Bn.test(n.data)&&"data");return l||"jsonp"===n.dataTypes[0]?(o=n.jsonpCallback=x.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,l?n[l]=n[l].replace(Bn,"$1"+o):n.jsonp!==!1&&(n.url+=(bn.test(n.url)?"&":"?")+n.jsonp+"="+o),n.converters["script json"]=function(){return s||x.error(o+" was not called"),s[0]},n.dataTypes[0]="json",a=e[o],e[o]=function(){s=arguments},i.always(function(){e[o]=a,n[o]&&(n.jsonpCallback=r.jsonpCallback,Fn.push(o)),s&&x.isFunction(a)&&a(s[0]),s=a=t}),"script"):t});var Pn,Rn,Wn=0,$n=e.ActiveXObject&&function(){var e;for(e in Pn)Pn[e](t,!0)};function In(){try{return new e.XMLHttpRequest}catch(t){}}function zn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}x.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&In()||zn()}:In,Rn=x.ajaxSettings.xhr(),x.support.cors=!!Rn&&"withCredentials"in Rn,Rn=x.support.ajax=!!Rn,Rn&&x.ajaxTransport(func
tion(n){if(!n.crossDomain||x.support.cors){var r;return{send:function(i,o){var a,s,l=n.xhr();if(n.username?l.open(n.type,n.url,n.async,n.username,n.password):l.open(n.type,n.url,n.async),n.xhrFields)for(s in n.xhrFields)l[s]=n.xhrFields[s];n.mimeType&&l.overrideMimeType&&l.overrideMimeType(n.mimeType),n.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");try{for(s in i)l.setRequestHeader(s,i[s])}catch(u){}l.send(n.hasContent&&n.data||null),r=function(e,i){var s,u,c,p;try{if(r&&(i||4===l.readyState))if(r=t,a&&(l.onreadystatechange=x.noop,$n&&delete Pn[a]),i)4!==l.readyState&&l.abort();else{p={},s=l.status,u=l.getAllResponseHeaders(),"string"==typeof l.responseText&&(p.text=l.responseText);try{c=l.statusText}catch(f){c=""}s||!n.isLocal||n.crossDomain?1223===s&&(s=204):s=p.text?200:404}}catch(d){i||o(-1,d)}p&&o(s,c,p,u)},n.async?4===l.readyState?setTimeout(r):(a=++Wn,$n&&(Pn||(Pn={},x(e).unload($n)),Pn[a]=r),l.onreadystatechange=r):r()},abort:function()
{r&&r(t,!0)}}}});var Xn,Un,Vn=/^(?:toggle|show|hide)$/,Yn=RegExp("^(?:([+-])=|)("+w+")([a-z%]*)$","i"),Jn=/queueHooks$/,Gn=[nr],Qn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=Yn.exec(t),o=i&&i[3]||(x.cssNumber[e]?"":"px"),a=(x.cssNumber[e]||"px"!==o&&+r)&&Yn.exec(x.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,x.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};function Kn(){return setTimeout(function(){Xn=t}),Xn=x.now()}function Zn(e,t,n){var r,i=(Qn[t]||[]).concat(Qn["*"]),o=0,a=i.length;for(;a>o;o++)if(r=i[o].call(n,t,e))return r}function er(e,t,n){var r,i,o=0,a=Gn.length,s=x.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;var t=Xn||Kn(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;for(;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u
]),!1)},u=s.promise({elem:e,props:x.extend({},t),opts:x.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:Xn||Kn(),duration:n.duration,tweens:[],createTween:function(t,n){var r=x.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(tr(c,u.opts.specialEasing);a>o;o++)if(r=Gn[o].call(u,e,c,u.opts))return r;return x.map(c,Zn,u),x.isFunction(u.opts.start)&&u.opts.start.call(e,u),x.fx.timer(x.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function tr(e,t){var n,r,i,o,a;for(n in e)if(r=x.camelCase(n),i=t[r],o=e[n],x.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=x.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n
in e||(e[n]=o[n],t[n]=i)}else t[r]=i}x.Animation=x.extend(er,{tweener:function(e,t){x.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;i>r;r++)n=e[r],Qn[n]=Qn[n]||[],Qn[n].unshift(t)},prefilter:function(e,t){t?Gn.unshift(e):Gn.push(e)}});function nr(e,t,n){var r,i,o,a,s,l,u=this,c={},p=e.style,f=e.nodeType&&nn(e),d=x._data(e,"fxshow");n.queue||(s=x._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,u.always(function(){u.always(function(){s.unqueued--,x.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],"inline"===x.css(e,"display")&&"none"===x.css(e,"float")&&(x.support.inlineBlockNeedsLayout&&"inline"!==ln(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",x.support.shrinkWrapBlocks||u.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.ove
rflow[2]}));for(r in t)if(i=t[r],Vn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(f?"hide":"show"))continue;c[r]=d&&d[r]||x.style(e,r)}if(!x.isEmptyObject(c)){d?"hidden"in d&&(f=d.hidden):d=x._data(e,"fxshow",{}),o&&(d.hidden=!f),f?x(e).show():u.done(function(){x(e).hide()}),u.done(function(){var t;x._removeData(e,"fxshow");for(t in c)x.style(e,t,c[t])});for(r in c)a=Zn(f?d[r]:0,r,u),r in d||(d[r]=a.start,f&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function rr(e,t,n,r,i){return new rr.prototype.init(e,t,n,r,i)}x.Tween=rr,rr.prototype={constructor:rr,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(x.cssNumber[n]?"":"px")},cur:function(){var e=rr.propHooks[this.prop];return e&&e.get?e.get(this):rr.propHooks._default.get(this)},run:function(e){var t,n=rr.propHooks[this.prop];return this.pos=t=this.options.duration?x.easing[this.easing](e,this.options.duration*e,0,1,t
his.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):rr.propHooks._default.set(this),this}},rr.prototype.init.prototype=rr.prototype,rr.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=x.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){x.fx.step[e.prop]?x.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[x.cssProps[e.prop]]||x.cssHooks[e.prop])?x.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},rr.propHooks.scrollTop=rr.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},x.each(["toggle","show","hide"],function(e,t){var n=x.fn[t];x.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ir(t,!0),e,r,i)}}),x.fn.extend({fadeTo:function(e,t,n,r){return this.filter(nn).css("opacity",0).show()
.end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=x.isEmptyObject(e),o=x.speed(t,n,r),a=function(){var t=er(this,x.extend({},e),o);(i||x._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return"string"!=typeof e&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=null!=e&&e+"queueHooks",o=x.timers,a=x._data(this);if(n)a[n]&&a[n].stop&&i(a[n]);else for(n in a)a[n]&&a[n].stop&&Jn.test(n)&&i(a[n]);for(n=o.length;n--;)o[n].elem!==this||null!=e&&o[n].queue!==e||(o[n].anim.stop(r),t=!1,o.splice(n,1));(t||!r)&&x.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=x._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=x.timers,a=r?r.length:0;for(n.finish=!0,x.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0)
,o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}});function ir(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=Zt[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}x.each({slideDown:ir("show"),slideUp:ir("hide"),slideToggle:ir("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){x.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),x.speed=function(e,t,n){var r=e&&"object"==typeof e?x.extend({},e):{complete:n||!n&&t||x.isFunction(e)&&e,duration:e,easing:n&&t||t&&!x.isFunction(t)&&t};return r.duration=x.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in x.fx.speeds?x.fx.speeds[r.duration]:x.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){x.isFunction(r.old)&&r.old.call(this),r.queue&&x.dequeue(this,r.queue)},r},x.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.
PI)/2}},x.timers=[],x.fx=rr.prototype.init,x.fx.tick=function(){var e,n=x.timers,r=0;for(Xn=x.now();n.length>r;r++)e=n[r],e()||n[r]!==e||n.splice(r--,1);n.length||x.fx.stop(),Xn=t},x.fx.timer=function(e){e()&&x.timers.push(e)&&x.fx.start()},x.fx.interval=13,x.fx.start=function(){Un||(Un=setInterval(x.fx.tick,x.fx.interval))},x.fx.stop=function(){clearInterval(Un),Un=null},x.fx.speeds={slow:600,fast:200,_default:400},x.fx.step={},x.expr&&x.expr.filters&&(x.expr.filters.animated=function(e){return x.grep(x.timers,function(t){return e===t.elem}).length}),x.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){x.offset.setOffset(this,e,t)});var n,r,o={top:0,left:0},a=this[0],s=a&&a.ownerDocument;if(s)return n=s.documentElement,x.contains(n,a)?(typeof a.getBoundingClientRect!==i&&(o=a.getBoundingClientRect()),r=or(s),{top:o.top+(r.pageYOffset||n.scrollTop)-(n.clientTop||0),left:o.left+(r.pageXOffset||n.scrollLeft)-(n.clientLeft||0)}):o},x.offset={setOf
fset:function(e,t,n){var r=x.css(e,"position");"static"===r&&(e.style.position="relative");var i=x(e),o=i.offset(),a=x.css(e,"top"),s=x.css(e,"left"),l=("absolute"===r||"fixed"===r)&&x.inArray("auto",[a,s])>-1,u={},c={},p,f;l?(c=i.position(),p=c.top,f=c.left):(p=parseFloat(a)||0,f=parseFloat(s)||0),x.isFunction(t)&&(t=t.call(e,n,o)),null!=t.top&&(u.top=t.top-o.top+p),null!=t.left&&(u.left=t.left-o.left+f),"using"in t?t.using.call(e,u):i.css(u)}},x.fn.extend({position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===x.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),x.nodeName(e[0],"html")||(n=e.offset()),n.top+=x.css(e[0],"borderTopWidth",!0),n.left+=x.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-x.css(r,"marginTop",!0),left:t.left-n.left-x.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||s;while(e&&!x.nodeName(e,"html")&&"static"===x.css(e,"position"))e=e.of
fsetParent;return e||s})}}),x.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);x.fn[e]=function(i){return x.access(this,function(e,i,o){var a=or(e);return o===t?a?n in a?a[n]:a.document.documentElement[i]:e[i]:(a?a.scrollTo(r?x(a).scrollLeft():o,r?o:x(a).scrollTop()):e[i]=o,t)},e,i,arguments.length,null)}});function or(e){return x.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}x.each({Height:"height",Width:"width"},function(e,n){x.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){x.fn[i]=function(i,o){var a=arguments.length&&(r||"boolean"!=typeof i),s=r||(i===!0||o===!0?"margin":"border");return x.access(this,function(n,r,i){var o;return x.isWindow(n)?n.document.documentElement["client"+e]:9===n.nodeType?(o=n.documentElement,Math.max(n.body["scroll"+e],o["scroll"+e],n.body["offset"+e],o["offset"+e],o["client"+e])):i===t?x.css(n,r,s):x.style(n,r,i,s)},n,a?i:t,a,null)}})}),x.fn.size=function(){return this.length
},x.fn.andSelf=x.fn.addBack,"object"==typeof module&&module&&"object"==typeof module.exports?module.exports=x:(e.jQuery=e.$=x,"function"==typeof define&&define.amd&&define("jquery",[],function(){return x}))})(window);
diff --git a/fedora_elections/static/jquery-ui-1.9.2.custom.min.js b/fedora_elections/static/jquery-ui-1.9.2.custom.min.js
new file mode 100644
index 0000000..15c62cd
--- /dev/null
+++ b/fedora_elections/static/jquery-ui-1.9.2.custom.min.js
@@ -0,0 +1,6 @@
+/*! jQuery UI - v1.9.2 - 2012-11-26
+* http://jqueryui.com
+* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.position.js, jquery.ui.draggable.js, jquery.ui.resizable.js, jquery.ui.accordion.js, jquery.ui.button.js, jquery.ui.datepicker.js, jquery.ui.dialog.js, jquery.ui.menu.js, jquery.ui.spinner.js
+* Copyright (c) 2012 jQuery Foundation and other contributors Licensed MIT */
+
+(function(e,t){function i(t,n){var r,i,o,u=t.nodeName.toLowerCase();return"area"===u?(r=t.parentNode,i=r.name,!t.href||!i||r.nodeName.toLowerCase()!=="map"?!1:(o=e("img[usemap=#"+i+"]")[0],!!o&&s(o))):(/input|select|textarea|button|object/.test(u)?!t.disabled:"a"===u?t.href||n:n)&&s(t)}function s(t){return e.expr.filters.visible(t)&&!e(t).parents().andSelf().filter(function(){return e.css(this,"visibility")==="hidden"}).length}var n=0,r=/^ui-id-\d+$/;e.ui=e.ui||{};if(e.ui.version)return;e.extend(e.ui,{version:"1.9.2",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({_focus:e.fn.focus,focus:function(t,n){return typeof t=="number"?this.each(function(){var r=this;setTimeout(function(){e(r).focus(),n&&n.call(r)},t)}):this._focus.apply(this,arguments)},
scrollParent:function(){var t;return e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?t=this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):t=this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(n){if(n!==t)return this.css("zIndex",n);if(this.length){var r=e(this[0]),i,s;while(r.length&&r[0]!==document){i=r.css("position");if(i==="absolute"||i==="relative"||i==="fixed"){s=parseInt(r.css("zIndex"),10);if(!isNaN(s)&&s!==0)return s}r=r.parent()}}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++n)})},removeUniqueId:function(){return this.each(function(){r.test(this.id)&&e(this).removeAtt
r("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(n){return!!e.data(n,t)}}):function(t,n,r){return!!e.data(t,r[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var n=e.attr(t,"tabindex"),r=isNaN(n);return(r||n>=0)&&i(t,!r)}}),e(function(){var t=document.body,n=t.appendChild(n=document.createElement("div"));n.offsetHeight,e.extend(n.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0}),e.support.minHeight=n.offsetHeight===100,e.support.selectstart="onselectstart"in n,t.removeChild(n).style.display="none"}),e("<a>").outerWidth(1).jquery||e.each(["Width","Height"],function(n,r){function u(t,n,r,s){return e.each(i,function(){n-=parseFloat(e.css(t,"padding"+this))||0,r&&(n-=parseFloat(e.css(t,"border"+this+"Width"))||0),s&&(n-=parseFloat(e.css(t,"margin"+this))||0)}),n}var i=r==="Width"?["Left","Right"]:["Top","Bottom"],s=r.toLowerCase(),o={innerWidth:e.fn.innerWidth,inne
rHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+r]=function(n){return n===t?o["inner"+r].call(this):this.each(function(){e(this).css(s,u(this,n)+"px")})},e.fn["outer"+r]=function(t,n){return typeof t!="number"?o["outer"+r].call(this,t):this.each(function(){e(this).css(s,u(this,t,!0,n)+"px")})}}),e("<a>").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(n){return arguments.length?t.call(this,e.camelCase(n)):t.call(this)}}(e.fn.removeData)),function(){var t=/msie ([\w.]+)/.exec(navigator.userAgent.toLowerCase())||[];e.ui.ie=t.length?!0:!1,e.ui.ie6=parseFloat(t[1],10)===6}(),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,n,r){var i,s=e.ui[t].prototype;for(i in r)s.
plugins[i]=s.plugins[i]||[],s.plugins[i].push([n,r[i]])},call:function(e,t,n){var r,i=e.plugins[t];if(!i||!e.element[0].parentNode||e.element[0].parentNode.nodeType===11)return;for(r=0;r<i.length;r++)e.options[i[r][0]]&&i[r][1].apply(e.element,n)}},contains:e.contains,hasScroll:function(t,n){if(e(t).css("overflow")==="hidden")return!1;var r=n&&n==="left"?"scrollLeft":"scrollTop",i=!1;return t[r]>0?!0:(t[r]=1,i=t[r]>0,t[r]=0,i)},isOverAxis:function(e,t,n){return e>t&&e<t+n},isOver:function(t,n,r,i,s,o){return e.ui.isOverAxis(t,r,s)&&e.ui.isOverAxis(n,i,o)}})})(jQuery);(function(e,t){var n=0,r=Array.prototype.slice,i=e.cleanData;e.cleanData=function(t){for(var n=0,r;(r=t[n])!=null;n++)try{e(r).triggerHandler("remove")}catch(s){}i(t)},e.widget=function(t,n,r){var i,s,o,u,a=t.split(".")[0];t=t.split(".")[1],i=a+"-"+t,r||(r=n,n=e.Widget),e.expr[":"][i.toLowerCase()]=function(t){return!!e.data(t,i)},e[a]=e[a]||{},s=e[a][t],o=e[a][t]=function(e,t){if(!this._createWidget)return new
o(e,t);arguments.length&&this._createWidget(e,t)},e.extend(o,s,{version:r.version,_proto:e.extend({},r),_childConstructors:[]}),u=new n,u.options=e.widget.extend({},u.options),e.each(r,function(t,i){e.isFunction(i)&&(r[t]=function(){var e=function(){return n.prototype[t].apply(this,arguments)},r=function(e){return n.prototype[t].apply(this,e)};return function(){var t=this._super,n=this._superApply,s;return this._super=e,this._superApply=r,s=i.apply(this,arguments),this._super=t,this._superApply=n,s}}())}),o.prototype=e.widget.extend(u,{widgetEventPrefix:s?u.widgetEventPrefix:t},r,{constructor:o,namespace:a,widgetName:t,widgetBaseClass:i,widgetFullName:i}),s?(e.each(s._childConstructors,function(t,n){var r=n.prototype;e.widget(r.namespace+"."+r.widgetName,o,n._proto)}),delete s._childConstructors):n._childConstructors.push(o),e.widget.bridge(t,o)},e.widget.extend=function(n){var i=r.call(arguments,1),s=0,o=i.length,u,a;for(;s<o;s++)for(u in i[s])a=i[s][u],i[s].hasOwnProperty(
u)&&a!==t&&(e.isPlainObject(a)?n[u]=e.isPlainObject(n[u])?e.widget.extend({},n[u],a):e.widget.extend({},a):n[u]=a);return n},e.widget.bridge=function(n,i){var s=i.prototype.widgetFullName||n;e.fn[n]=function(o){var u=typeof o=="string",a=r.call(arguments,1),f=this;return o=!u&&a.length?e.widget.extend.apply(null,[o].concat(a)):o,u?this.each(function(){var r,i=e.data(this,s);if(!i)return e.error("cannot call methods on "+n+" prior to initialization; "+"attempted to call method '"+o+"'");if(!e.isFunction(i[o])||o.charAt(0)==="_")return e.error("no such method '"+o+"' for "+n+" widget instance");r=i[o].apply(i,a);if(r!==i&&r!==t)return f=r&&r.jquery?f.pushStack(r.get()):r,!1}):this.each(function(){var t=e.data(this,s);t?t.option(o||{})._init():e.data(this,s,new i(o,this))}),f}},e.Widget=function(){},e.Widget._childConstructors=[],e.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"<div>",options:{disabled:!1,create:null},_createWidget:function(t,r){r=e(
r||this.defaultElement||this)[0],this.element=e(r),this.uuid=n++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=e.widget.extend({},this.options,this._getCreateOptions(),t),this.bindings=e(),this.hoverable=e(),this.focusable=e(),r!==this&&(e.data(r,this.widgetName,this),e.data(r,this.widgetFullName,this),this._on(!0,this.element,{remove:function(e){e.target===r&&this.destroy()}}),this.document=e(r.style?r.ownerDocument:r.document||r),this.window=e(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:e.noop,_getCreateEventData:e.noop,_create:e.noop,_init:e.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(e.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "
+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:e.noop,widget:function(){return this.element},option:function(n,r){var i=n,s,o,u;if(arguments.length===0)return e.widget.extend({},this.options);if(typeof n=="string"){i={},s=n.split("."),n=s.shift();if(s.length){o=i[n]=e.widget.extend({},this.options[n]);for(u=0;u<s.length-1;u++)o[s[u]]=o[s[u]]||{},o=o[s[u]];n=s.pop();if(r===t)return o[n]===t?null:o[n];o[n]=r}else{if(r===t)return this.options[n]===t?null:this.options[n];i[n]=r}}return this._setOptions(i),this},_setOptions:function(e){var t;for(t in e)this._setOption(t,e[t]);return this},_setOption:function(e,t){return this.options[e]=t,e==="disabled"&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!t).attr("aria-disabled",t),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:funct
ion(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(t,n,r){var i,s=this;typeof t!="boolean"&&(r=n,n=t,t=!1),r?(n=i=e(n),this.bindings=this.bindings.add(n)):(r=n,n=this.element,i=this.widget()),e.each(r,function(r,o){function u(){if(!t&&(s.options.disabled===!0||e(this).hasClass("ui-state-disabled")))return;return(typeof o=="string"?s[o]:o).apply(s,arguments)}typeof o!="string"&&(u.guid=o.guid=o.guid||u.guid||e.guid++);var a=r.match(/^(\w+)\s*(.*)$/),f=a[1]+s.eventNamespace,l=a[2];l?i.delegate(l,f,u):n.bind(f,u)})},_off:function(e,t){t=(t||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.unbind(t).undelegate(t)},_delay:function(e,t){function n(){return(typeof e=="string"?r[e]:e).apply(r,arguments)}var r=this;return setTimeout(n,t||0)},_hoverable:function(t){this.hoverable=this.hoverable.add(t),this._on(t,{mouseenter:function(t){e(t.currentTarget).addClass("ui-state-hover")},mouseleave:function(t
){e(t.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(t){this.focusable=this.focusable.add(t),this._on(t,{focusin:function(t){e(t.currentTarget).addClass("ui-state-focus")},focusout:function(t){e(t.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(t,n,r){var i,s,o=this.options[t];r=r||{},n=e.Event(n),n.type=(t===this.widgetEventPrefix?t:this.widgetEventPrefix+t).toLowerCase(),n.target=this.element[0],s=n.originalEvent;if(s)for(i in s)i in n||(n[i]=s[i]);return this.element.trigger(n,r),!(e.isFunction(o)&&o.apply(this.element[0],[n].concat(r))===!1||n.isDefaultPrevented())}},e.each({show:"fadeIn",hide:"fadeOut"},function(t,n){e.Widget.prototype["_"+t]=function(r,i,s){typeof i=="string"&&(i={effect:i});var o,u=i?i===!0||typeof i=="number"?n:i.effect||n:t;i=i||{},typeof i=="number"&&(i={duration:i}),o=!e.isEmptyObject(i),i.complete=s,i.delay&&r.delay(i.delay),o&&e.effects&&(e.effects.effect[u]||e.uiBackCompat!==!1&&e.effects[u])?r[t](i):u!=
=t&&r[u]?r[u](i.duration,i.easing,s):r.queue(function(n){e(this)[t](),s&&s.call(r[0]),n()})}}),e.uiBackCompat!==!1&&(e.Widget.prototype._getCreateOptions=function(){return e.metadata&&e.metadata.get(this.element[0])[this.widgetName]})})(jQuery);(function(e,t){var n=!1;e(document).mouseup(function(e){n=!1}),e.widget("ui.mouse",{version:"1.9.2",options:{cancel:"input,textarea,button,select,option",distance:1,delay:0},_mouseInit:function(){var t=this;this.element.bind("mousedown."+this.widgetName,function(e){return t._mouseDown(e)}).bind("click."+this.widgetName,function(n){if(!0===e.data(n.target,t.widgetName+".preventClickEvent"))return e.removeData(n.target,t.widgetName+".preventClickEvent"),n.stopImmediatePropagation(),!1}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&e(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:func
tion(t){if(n)return;this._mouseStarted&&this._mouseUp(t),this._mouseDownEvent=t;var r=this,i=t.which===1,s=typeof this.options.cancel=="string"&&t.target.nodeName?e(t.target).closest(this.options.cancel).length:!1;if(!i||s||!this._mouseCapture(t))return!0;this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){r.mouseDelayMet=!0},this.options.delay));if(this._mouseDistanceMet(t)&&this._mouseDelayMet(t)){this._mouseStarted=this._mouseStart(t)!==!1;if(!this._mouseStarted)return t.preventDefault(),!0}return!0===e.data(t.target,this.widgetName+".preventClickEvent")&&e.removeData(t.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(e){return r._mouseMove(e)},this._mouseUpDelegate=function(e){return r._mouseUp(e)},e(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),t.preventDefault(),n=!0,!0},_mouseMove:function(t){return!e.ui.ie||doc
ument.documentMode>=9||!!t.button?this._mouseStarted?(this._mouseDrag(t),t.preventDefault()):(this._mouseDistanceMet(t)&&this._mouseDelayMet(t)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,t)!==!1,this._mouseStarted?this._mouseDrag(t):this._mouseUp(t)),!this._mouseStarted):this._mouseUp(t)},_mouseUp:function(t){return e(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,t.target===this._mouseDownEvent.target&&e.data(t.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(t)),!1},_mouseDistanceMet:function(e){return Math.max(Math.abs(this._mouseDownEvent.pageX-e.pageX),Math.abs(this._mouseDownEvent.pageY-e.pageY))>=this.options.distance},_mouseDelayMet:function(e){return this.mouseDelayMet},_mouseStart:function(e){},_mouseDrag:function(e){},_mouseStop:function(e){},_mouseCapture:function(e){return!0}})})(jQuery);(function(e,t){functio
n h(e,t,n){return[parseInt(e[0],10)*(l.test(e[0])?t/100:1),parseInt(e[1],10)*(l.test(e[1])?n/100:1)]}function p(t,n){return parseInt(e.css(t,n),10)||0}e.ui=e.ui||{};var n,r=Math.max,i=Math.abs,s=Math.round,o=/left|center|right/,u=/top|center|bottom/,a=/[\+\-]\d+%?/,f=/^\w+/,l=/%$/,c=e.fn.position;e.position={scrollbarWidth:function(){if(n!==t)return n;var r,i,s=e("<div style='display:block;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>"),o=s.children()[0];return e("body").append(s),r=o.offsetWidth,s.css("overflow","scroll"),i=o.offsetWidth,r===i&&(i=s[0].clientWidth),s.remove(),n=r-i},getScrollInfo:function(t){var n=t.isWindow?"":t.element.css("overflow-x"),r=t.isWindow?"":t.element.css("overflow-y"),i=n==="scroll"||n==="auto"&&t.width<t.element[0].scrollWidth,s=r==="scroll"||r==="auto"&&t.height<t.element[0].scrollHeight;return{width:i?e.position.scrollbarWidth():0,height:s?e.position.scrollbarWidth():0}},getWithinInfo:function(t
){var n=e(t||window),r=e.isWindow(n[0]);return{element:n,isWindow:r,offset:n.offset()||{left:0,top:0},scrollLeft:n.scrollLeft(),scrollTop:n.scrollTop(),width:r?n.width():n.outerWidth(),height:r?n.height():n.outerHeight()}}},e.fn.position=function(t){if(!t||!t.of)return c.apply(this,arguments);t=e.extend({},t);var n,l,d,v,m,g=e(t.of),y=e.position.getWithinInfo(t.within),b=e.position.getScrollInfo(y),w=g[0],E=(t.collision||"flip").split(" "),S={};return w.nodeType===9?(l=g.width(),d=g.height(),v={top:0,left:0}):e.isWindow(w)?(l=g.width(),d=g.height(),v={top:g.scrollTop(),left:g.scrollLeft()}):w.preventDefault?(t.at="left top",l=d=0,v={top:w.pageY,left:w.pageX}):(l=g.outerWidth(),d=g.outerHeight(),v=g.offset()),m=e.extend({},v),e.each(["my","at"],function(){var e=(t[this]||"").split(" "),n,r;e.length===1&&(e=o.test(e[0])?e.concat(["center"]):u.test(e[0])?["center"].concat(e):["center","center"]),e[0]=o.test(e[0])?e[0]:"center",e[1]=u.test(e[1])?e[1]:"center",n=a.exec(e[0]),r=a.
exec(e[1]),S[this]=[n?n[0]:0,r?r[0]:0],t[this]=[f.exec(e[0])[0],f.exec(e[1])[0]]}),E.length===1&&(E[1]=E[0]),t.at[0]==="right"?m.left+=l:t.at[0]==="center"&&(m.left+=l/2),t.at[1]==="bottom"?m.top+=d:t.at[1]==="center"&&(m.top+=d/2),n=h(S.at,l,d),m.left+=n[0],m.top+=n[1],this.each(function(){var o,u,a=e(this),f=a.outerWidth(),c=a.outerHeight(),w=p(this,"marginLeft"),x=p(this,"marginTop"),T=f+w+p(this,"marginRight")+b.width,N=c+x+p(this,"marginBottom")+b.height,C=e.extend({},m),k=h(S.my,a.outerWidth(),a.outerHeight());t.my[0]==="right"?C.left-=f:t.my[0]==="center"&&(C.left-=f/2),t.my[1]==="bottom"?C.top-=c:t.my[1]==="center"&&(C.top-=c/2),C.left+=k[0],C.top+=k[1],e.support.offsetFractions||(C.left=s(C.left),C.top=s(C.top)),o={marginLeft:w,marginTop:x},e.each(["left","top"],function(r,i){e.ui.position[E[r]]&&e.ui.position[E[r]][i](C,{targetWidth:l,targetHeight:d,elemWidth:f,elemHeight:c,collisionPosition:o,collisionWidth:T,collisionHeight:N,offset:[n[0]+k[0],n[1]+k[1]],my:t.my,
at:t.at,within:y,elem:a})}),e.fn.bgiframe&&a.bgiframe(),t.using&&(u=function(e){var n=v.left-C.left,s=n+l-f,o=v.top-C.top,u=o+d-c,h={target:{element:g,left:v.left,top:v.top,width:l,height:d},element:{element:a,left:C.left,top:C.top,width:f,height:c},horizontal:s<0?"left":n>0?"right":"center",vertical:u<0?"top":o>0?"bottom":"middle"};l<f&&i(n+s)<l&&(h.horizontal="center"),d<c&&i(o+u)<d&&(h.vertical="middle"),r(i(n),i(s))>r(i(o),i(u))?h.important="horizontal":h.important="vertical",t.using.call(this,e,h)}),a.offset(e.extend(C,{using:u}))})},e.ui.position={fit:{left:function(e,t){var n=t.within,i=n.isWindow?n.scrollLeft:n.offset.left,s=n.width,o=e.left-t.collisionPosition.marginLeft,u=i-o,a=o+t.collisionWidth-s-i,f;t.collisionWidth>s?u>0&&a<=0?(f=e.left+u+t.collisionWidth-s-i,e.left+=u-f):a>0&&u<=0?e.left=i:u>a?e.left=i+s-t.collisionWidth:e.left=i:u>0?e.left+=u:a>0?e.left-=a:e.left=r(e.left-o,e.left)},top:function(e,t){var n=t.within,i=n.isWindow?n.scrollTop:n.offset.top,s=t.wi
thin.height,o=e.top-t.collisionPosition.marginTop,u=i-o,a=o+t.collisionHeight-s-i,f;t.collisionHeight>s?u>0&&a<=0?(f=e.top+u+t.collisionHeight-s-i,e.top+=u-f):a>0&&u<=0?e.top=i:u>a?e.top=i+s-t.collisionHeight:e.top=i:u>0?e.top+=u:a>0?e.top-=a:e.top=r(e.top-o,e.top)}},flip:{left:function(e,t){var n=t.within,r=n.offset.left+n.scrollLeft,s=n.width,o=n.isWindow?n.scrollLeft:n.offset.left,u=e.left-t.collisionPosition.marginLeft,a=u-o,f=u+t.collisionWidth-s-o,l=t.my[0]==="left"?-t.elemWidth:t.my[0]==="right"?t.elemWidth:0,c=t.at[0]==="left"?t.targetWidth:t.at[0]==="right"?-t.targetWidth:0,h=-2*t.offset[0],p,d;if(a<0){p=e.left+l+c+h+t.collisionWidth-s-r;if(p<0||p<i(a))e.left+=l+c+h}else if(f>0){d=e.left-t.collisionPosition.marginLeft+l+c+h-o;if(d>0||i(d)<f)e.left+=l+c+h}},top:function(e,t){var n=t.within,r=n.offset.top+n.scrollTop,s=n.height,o=n.isWindow?n.scrollTop:n.offset.top,u=e.top-t.collisionPosition.marginTop,a=u-o,f=u+t.collisionHeight-s-o,l=t.my[1]==="top",c=l?-t.elemHeigh
t:t.my[1]==="bottom"?t.elemHeight:0,h=t.at[1]==="top"?t.targetHeight:t.at[1]==="bottom"?-t.targetHeight:0,p=-2*t.offset[1],d,v;a<0?(v=e.top+c+h+p+t.collisionHeight-s-r,e.top+c+h+p>a&&(v<0||v<i(a))&&(e.top+=c+h+p)):f>0&&(d=e.top-t.collisionPosition.marginTop+c+h+p-o,e.top+c+h+p>f&&(d>0||i(d)<f)&&(e.top+=c+h+p))}},flipfit:{left:function(){e.ui.position.flip.left.apply(this,arguments),e.ui.position.fit.left.apply(this,arguments)},top:function(){e.ui.position.flip.top.apply(this,arguments),e.ui.position.fit.top.apply(this,arguments)}}},function(){var t,n,r,i,s,o=document.getElementsByTagName("body")[0],u=document.createElement("div");t=document.createElement(o?"div":"body"),r={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},o&&e.extend(r,{position:"absolute",left:"-1000px",top:"-1000px"});for(s in r)t.style[s]=r[s];t.appendChild(u),n=o||document.documentElement,n.insertBefore(t,n.firstChild),u.style.cssText="position: absolute; left: 10.7432222px;",i=e(
u).offset().left,e.support.offsetFractions=i>10&&i<11,t.innerHTML="",n.removeChild(t)}(),e.uiBackCompat!==!1&&function(e){var n=e.fn.position;e.fn.position=function(r){if(!r||!r.offset)return n.call(this,r);var i=r.offset.split(" "),s=r.at.split(" ");return i.length===1&&(i[1]=i[0]),/^\d/.test(i[0])&&(i[0]="+"+i[0]),/^\d/.test(i[1])&&(i[1]="+"+i[1]),s.length===1&&(/left|center|right/.test(s[0])?s[1]="center":(s[1]=s[0],s[0]="center")),n.call(this,e.extend(r,{at:s[0]+i[0]+" "+s[1]+i[1],offset:t}))}}(jQuery)})(jQuery);(function(e,t){e.widget("ui.draggable",e.ui.mouse,{version:"1.9.2",widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1},_create:function(){this.options.
helper=="original"&&!/^(?:r|a|f)/.test(this.element.css("position"))&&(this.element[0].style.position="relative"),this.options.addClasses&&this.element.addClass("ui-draggable"),this.options.disabled&&this.element.addClass("ui-draggable-disabled"),this._mouseInit()},_destroy:function(){this.element.removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"),this._mouseDestroy()},_mouseCapture:function(t){var n=this.options;return this.helper||n.disabled||e(t.target).is(".ui-resizable-handle")?!1:(this.handle=this._getHandle(t),this.handle?(e(n.iframeFix===!0?"iframe":n.iframeFix).each(function(){e('<div class="ui-draggable-iframeFix" style="background: #fff;"></div>').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1e3}).css(e(this).offset()).appendTo("body")}),!0):!1)},_mouseStart:function(t){var n=this.options;return this.helper=this._createHelper(t),this.helper.addClass("ui-draggable-dragging"),this._cac
heHelperProportions(),e.ui.ddmanager&&(e.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(),this.offset=this.positionAbs=this.element.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},e.extend(this.offset,{click:{left:t.pageX-this.offset.left,top:t.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.originalPosition=this.position=this._generatePosition(t),this.originalPageX=t.pageX,this.originalPageY=t.pageY,n.cursorAt&&this._adjustOffsetFromHelper(n.cursorAt),n.containment&&this._setContainment(),this._trigger("start",t)===!1?(this._clear(),!1):(this._cacheHelperProportions(),e.ui.ddmanager&&!n.dropBehaviour&&e.ui.ddmanager.prepareOffsets(this,t),this._mouseDrag(t,!0),e.ui.ddmanager&&e.ui.ddmanager.dragStart(this,t),!0)},_mouseDrag:function(t,n){this.position=this._generatePosition(t),this.positi
onAbs=this._convertPositionTo("absolute");if(!n){var r=this._uiHash();if(this._trigger("drag",t,r)===!1)return this._mouseUp({}),!1;this.position=r.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";return e.ui.ddmanager&&e.ui.ddmanager.drag(this,t),!1},_mouseStop:function(t){var n=!1;e.ui.ddmanager&&!this.options.dropBehaviour&&(n=e.ui.ddmanager.drop(this,t)),this.dropped&&(n=this.dropped,this.dropped=!1);var r=this.element[0],i=!1;while(r&&(r=r.parentNode))r==document&&(i=!0);if(!i&&this.options.helper==="original")return!1;if(this.options.revert=="invalid"&&!n||this.options.revert=="valid"&&n||this.options.revert===!0||e.isFunction(this.options.revert)&&this.options.revert.call(this.element,n)){var s=this;e(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){s._trigger("stop",t)!==!1&
&s._clear()})}else this._trigger("stop",t)!==!1&&this._clear();return!1},_mouseUp:function(t){return e("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)}),e.ui.ddmanager&&e.ui.ddmanager.dragStop(this,t),e.ui.mouse.prototype._mouseUp.call(this,t)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear(),this},_getHandle:function(t){var n=!this.options.handle||!e(this.options.handle,this.element).length?!0:!1;return e(this.options.handle,this.element).find("*").andSelf().each(function(){this==t.target&&(n=!0)}),n},_createHelper:function(t){var n=this.options,r=e.isFunction(n.helper)?e(n.helper.apply(this.element[0],[t])):n.helper=="clone"?this.element.clone().removeAttr("id"):this.element;return r.parents("body").length||r.appendTo(n.appendTo=="parent"?this.element[0].parentNode:n.appendTo),r[0]!=this.element[0]&&!/(fixed|absolute)/.test(r.css("position"))&&r.css("position","absolute"),r},_adjustOffsetFromH
elper:function(t){typeof t=="string"&&(t=t.split(" ")),e.isArray(t)&&(t={left:+t[0],top:+t[1]||0}),"left"in t&&(this.offset.click.left=t.left+this.margins.left),"right"in t&&(this.offset.click.left=this.helperProportions.width-t.right+this.margins.left),"top"in t&&(this.offset.click.top=t.top+this.margins.top),"bottom"in t&&(this.offset.click.top=this.helperProportions.height-t.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var t=this.offsetParent.offset();this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&e.contains(this.scrollParent[0],this.offsetParent[0])&&(t.left+=this.scrollParent.scrollLeft(),t.top+=this.scrollParent.scrollTop());if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&e.ui.ie)t={top:0,left:0};return{top:t.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:t.left+(parseInt(this.offsetParent.css("borderLeftWidt
h"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var e=this.element.position();return{top:e.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:e.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var t=this.options;t.containment=="parent"&&(t.containment=this.helper[0].parentNode);if(t.containment=="document"||t.containment=="window")this.containment=[t.containment=="document"?0:e(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,t.containment=="docume
nt"?0:e(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,(t.containment=="document"?0:e(window).scrollLeft())+e(t.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(t.containment=="document"?0:e(window).scrollTop())+(e(t.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(t.containment)&&t.containment.constructor!=Array){var n=e(t.containment),r=n[0];if(!r)return;var i=n.offset(),s=e(r).css("overflow")!="hidden";this.containment=[(parseInt(e(r).css("borderLeftWidth"),10)||0)+(parseInt(e(r).css("paddingLeft"),10)||0),(parseInt(e(r).css("borderTopWidth"),10)||0)+(parseInt(e(r).css("paddingTop"),10)||0),(s?Math.max(r.scrollWidth,r.offsetWidth):r.offsetWidth)-(parseInt(e(r).css("borderLeftWidth"),10)||0)-(parseInt(e(r).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-
this.margins.right,(s?Math.max(r.scrollHeight,r.offsetHeight):r.offsetHeight)-(parseInt(e(r).css("borderTopWidth"),10)||0)-(parseInt(e(r).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relative_container=n}else t.containment.constructor==Array&&(this.containment=t.containment)},_convertPositionTo:function(t,n){n||(n=this.position);var r=t=="absolute"?1:-1,i=this.options,s=this.cssPosition!="absolute"||this.scrollParent[0]!=document&&!!e.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,o=/(html|body)/i.test(s[0].tagName);return{top:n.top+this.offset.relative.top*r+this.offset.parent.top*r-(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():o?0:s.scrollTop())*r,left:n.left+this.offset.relative.left*r+this.offset.parent.left*r-(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():o?0:s.scrollLeft())*r}},_generatePosition:function(t){var n=this.options,r=this.cssPosition!="ab
solute"||this.scrollParent[0]!=document&&!!e.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,i=/(html|body)/i.test(r[0].tagName),s=t.pageX,o=t.pageY;if(this.originalPosition){var u;if(this.containment){if(this.relative_container){var a=this.relative_container.offset();u=[this.containment[0]+a.left,this.containment[1]+a.top,this.containment[2]+a.left,this.containment[3]+a.top]}else u=this.containment;t.pageX-this.offset.click.left<u[0]&&(s=u[0]+this.offset.click.left),t.pageY-this.offset.click.top<u[1]&&(o=u[1]+this.offset.click.top),t.pageX-this.offset.click.left>u[2]&&(s=u[2]+this.offset.click.left),t.pageY-this.offset.click.top>u[3]&&(o=u[3]+this.offset.click.top)}if(n.grid){var f=n.grid[1]?this.originalPageY+Math.round((o-this.originalPageY)/n.grid[1])*n.grid[1]:this.originalPageY;o=u?f-this.offset.click.top<u[1]||f-this.offset.click.top>u[3]?f-this.offset.click.top<u[1]?f+n.grid[1]:f-n.grid[1]:f:f;var l=n.grid[0]?this.originalPageX
+Math.round((s-this.originalPageX)/n.grid[0])*n.grid[0]:this.originalPageX;s=u?l-this.offset.click.left<u[0]||l-this.offset.click.left>u[2]?l-this.offset.click.left<u[0]?l+n.grid[0]:l-n.grid[0]:l:l}}return{top:o-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():i?0:r.scrollTop()),left:s-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():i?0:r.scrollLeft())}},_clear:function(){this.helper.removeClass("ui-draggable-dragging"),this.helper[0]!=this.element[0]&&!this.cancelHelperRemoval&&this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1},_trigger:function(t,n,r){return r=r||this._uiHash(),e.ui.plugin.call(this,t,[n,r]),t=="drag"&&(this.positionAbs=this._convertPositionTo("absolute")),e.Widget.prototype._trigger.call(this,t,n,r)},plugins:{},_uiHash:function(e){return{helper:this.helper,position:this.position,
originalPosition:this.originalPosition,offset:this.positionAbs}}}),e.ui.plugin.add("draggable","connectToSortable",{start:function(t,n){var r=e(this).data("draggable"),i=r.options,s=e.extend({},n,{item:r.element});r.sortables=[],e(i.connectToSortable).each(function(){var n=e.data(this,"sortable");n&&!n.options.disabled&&(r.sortables.push({instance:n,shouldRevert:n.options.revert}),n.refreshPositions(),n._trigger("activate",t,s))})},stop:function(t,n){var r=e(this).data("draggable"),i=e.extend({},n,{item:r.element});e.each(r.sortables,function(){this.instance.isOver?(this.instance.isOver=0,r.cancelHelperRemoval=!0,this.instance.cancelHelperRemoval=!1,this.shouldRevert&&(this.instance.options.revert=!0),this.instance._mouseStop(t),this.instance.options.helper=this.instance.options._helper,r.options.helper=="original"&&this.instance.currentItem.css({top:"auto",left:"auto"})):(this.instance.cancelHelperRemoval=!1,this.instance._trigger("deactivate",t,i))})},drag:function(t,n){va
r r=e(this).data("draggable"),i=this,s=function(t){var n=this.offset.click.top,r=this.offset.click.left,i=this.positionAbs.top,s=this.positionAbs.left,o=t.height,u=t.width,a=t.top,f=t.left;return e.ui.isOver(i+n,s+r,a,f,o,u)};e.each(r.sortables,function(s){var o=!1,u=this;this.instance.positionAbs=r.positionAbs,this.instance.helperProportions=r.helperProportions,this.instance.offset.click=r.offset.click,this.instance._intersectsWith(this.instance.containerCache)&&(o=!0,e.each(r.sortables,function(){return this.instance.positionAbs=r.positionAbs,this.instance.helperProportions=r.helperProportions,this.instance.offset.click=r.offset.click,this!=u&&this.instance._intersectsWith(this.instance.containerCache)&&e.ui.contains(u.instance.element[0],this.instance.element[0])&&(o=!1),o})),o?(this.instance.isOver||(this.instance.isOver=1,this.instance.currentItem=e(i).clone().removeAttr("id").appendTo(this.instance.element).data("sortable-item",!0),this.instance.options._helper=this.in
stance.options.helper,this.instance.options.helper=function(){return n.helper[0]},t.target=this.instance.currentItem[0],this.instance._mouseCapture(t,!0),this.instance._mouseStart(t,!0,!0),this.instance.offset.click.top=r.offset.click.top,this.instance.offset.click.left=r.offset.click.left,this.instance.offset.parent.left-=r.offset.parent.left-this.instance.offset.parent.left,this.instance.offset.parent.top-=r.offset.parent.top-this.instance.offset.parent.top,r._trigger("toSortable",t),r.dropped=this.instance.element,r.currentItem=r.element,this.instance.fromOutside=r),this.instance.currentItem&&this.instance._mouseDrag(t)):this.instance.isOver&&(this.instance.isOver=0,this.instance.cancelHelperRemoval=!0,this.instance.options.revert=!1,this.instance._trigger("out",t,this.instance._uiHash(this.instance)),this.instance._mouseStop(t,!0),this.instance.options.helper=this.instance.options._helper,this.instance.currentItem.remove(),this.instance.placeholder&&this.instance.placeho
lder.remove(),r._trigger("fromSortable",t),r.dropped=!1)})}}),e.ui.plugin.add("draggable","cursor",{start:function(t,n){var r=e("body"),i=e(this).data("draggable").options;r.css("cursor")&&(i._cursor=r.css("cursor")),r.css("cursor",i.cursor)},stop:function(t,n){var r=e(this).data("draggable").options;r._cursor&&e("body").css("cursor",r._cursor)}}),e.ui.plugin.add("draggable","opacity",{start:function(t,n){var r=e(n.helper),i=e(this).data("draggable").options;r.css("opacity")&&(i._opacity=r.css("opacity")),r.css("opacity",i.opacity)},stop:function(t,n){var r=e(this).data("draggable").options;r._opacity&&e(n.helper).css("opacity",r._opacity)}}),e.ui.plugin.add("draggable","scroll",{start:function(t,n){var r=e(this).data("draggable");r.scrollParent[0]!=document&&r.scrollParent[0].tagName!="HTML"&&(r.overflowOffset=r.scrollParent.offset())},drag:function(t,n){var r=e(this).data("draggable"),i=r.options,s=!1;if(r.scrollParent[0]!=document&&r.scrollParent[0].tagName!="HTML"){if(!i
.axis||i.axis!="x")r.overflowOffset.top+r.scrollParent[0].offsetHeight-t.pageY<i.scrollSensitivity?r.scrollParent[0].scrollTop=s=r.scrollParent[0].scrollTop+i.scrollSpeed:t.pageY-r.overflowOffset.top<i.scrollSensitivity&&(r.scrollParent[0].scrollTop=s=r.scrollParent[0].scrollTop-i.scrollSpeed);if(!i.axis||i.axis!="y")r.overflowOffset.left+r.scrollParent[0].offsetWidth-t.pageX<i.scrollSensitivity?r.scrollParent[0].scrollLeft=s=r.scrollParent[0].scrollLeft+i.scrollSpeed:t.pageX-r.overflowOffset.left<i.scrollSensitivity&&(r.scrollParent[0].scrollLeft=s=r.scrollParent[0].scrollLeft-i.scrollSpeed)}else{if(!i.axis||i.axis!="x")t.pageY-e(document).scrollTop()<i.scrollSensitivity?s=e(document).scrollTop(e(document).scrollTop()-i.scrollSpeed):e(window).height()-(t.pageY-e(document).scrollTop())<i.scrollSensitivity&&(s=e(document).scrollTop(e(document).scrollTop()+i.scrollSpeed));if(!i.axis||i.axis!="y")t.pageX-e(document).scrollLeft()<i.scrollSensitivity?s=e(document).scrollLeft(e(do
cument).scrollLeft()-i.scrollSpeed):e(window).width()-(t.pageX-e(document).scrollLeft())<i.scrollSensitivity&&(s=e(document).scrollLeft(e(document).scrollLeft()+i.scrollSpeed))}s!==!1&&e.ui.ddmanager&&!i.dropBehaviour&&e.ui.ddmanager.prepareOffsets(r,t)}}),e.ui.plugin.add("draggable","snap",{start:function(t,n){var r=e(this).data("draggable"),i=r.options;r.snapElements=[],e(i.snap.constructor!=String?i.snap.items||":data(draggable)":i.snap).each(function(){var t=e(this),n=t.offset();this!=r.element[0]&&r.snapElements.push({item:this,width:t.outerWidth(),height:t.outerHeight(),top:n.top,left:n.left})})},drag:function(t,n){var r=e(this).data("draggable"),i=r.options,s=i.snapTolerance,o=n.offset.left,u=o+r.helperProportions.width,a=n.offset.top,f=a+r.helperProportions.height;for(var l=r.snapElements.length-1;l>=0;l--){var c=r.snapElements[l].left,h=c+r.snapElements[l].width,p=r.snapElements[l].top,d=p+r.snapElements[l].height;if(!(c-s<o&&o<h+s&&p-s<a&&a<d+s||c-s<o&&o<h+s&&p-s<f
&&f<d+s||c-s<u&&u<h+s&&p-s<a&&a<d+s||c-s<u&&u<h+s&&p-s<f&&f<d+s)){r.snapElements[l].snapping&&r.options.snap.release&&r.options.snap.release.call(r.element,t,e.extend(r._uiHash(),{snapItem:r.snapElements[l].item})),r.snapElements[l].snapping=!1;continue}if(i.snapMode!="inner"){var v=Math.abs(p-f)<=s,m=Math.abs(d-a)<=s,g=Math.abs(c-u)<=s,y=Math.abs(h-o)<=s;v&&(n.position.top=r._convertPositionTo("relative",{top:p-r.helperProportions.height,left:0}).top-r.margins.top),m&&(n.position.top=r._convertPositionTo("relative",{top:d,left:0}).top-r.margins.top),g&&(n.position.left=r._convertPositionTo("relative",{top:0,left:c-r.helperProportions.width}).left-r.margins.left),y&&(n.position.left=r._convertPositionTo("relative",{top:0,left:h}).left-r.margins.left)}var b=v||m||g||y;if(i.snapMode!="outer"){var v=Math.abs(p-a)<=s,m=Math.abs(d-f)<=s,g=Math.abs(c-o)<=s,y=Math.abs(h-u)<=s;v&&(n.position.top=r._convertPositionTo("relative",{top:p,left:0}).top-r.margins.top),m&&(n.position.top=r.
_convertPositionTo("relative",{top:d-r.helperProportions.height,left:0}).top-r.margins.top),g&&(n.position.left=r._convertPositionTo("relative",{top:0,left:c}).left-r.margins.left),y&&(n.position.left=r._convertPositionTo("relative",{top:0,left:h-r.helperProportions.width}).left-r.margins.left)}!r.snapElements[l].snapping&&(v||m||g||y||b)&&r.options.snap.snap&&r.options.snap.snap.call(r.element,t,e.extend(r._uiHash(),{snapItem:r.snapElements[l].item})),r.snapElements[l].snapping=v||m||g||y||b}}}),e.ui.plugin.add("draggable","stack",{start:function(t,n){var r=e(this).data("draggable").options,i=e.makeArray(e(r.stack)).sort(function(t,n){return(parseInt(e(t).css("zIndex"),10)||0)-(parseInt(e(n).css("zIndex"),10)||0)});if(!i.length)return;var s=parseInt(i[0].style.zIndex)||0;e(i).each(function(e){this.style.zIndex=s+e}),this[0].style.zIndex=s+i.length}}),e.ui.plugin.add("draggable","zIndex",{start:function(t,n){var r=e(n.helper),i=e(this).data("draggable").options;r.css("zIndex
")&&(i._zIndex=r.css("zIndex")),r.css("zIndex",i.zIndex)},stop:function(t,n){var r=e(this).data("draggable").options;r._zIndex&&e(n.helper).css("zIndex",r._zIndex)}})})(jQuery);(function(e,t){e.widget("ui.resizable",e.ui.mouse,{version:"1.9.2",widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:1e3},_create:function(){var t=this,n=this.options;this.element.addClass("ui-resizable"),e.extend(this,{_aspectRatio:!!n.aspectRatio,aspectRatio:n.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:n.helper||n.ghost||n.animate?n.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)&&(this.element.wrap(e('<div class="ui-wrapper" style="overflow: hidden;"></div>').css({position:this.element.css("positio
n"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("resizable",this.element.data("resizable")),this.elementIsWrapper=!0,this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")}),this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0}),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css({margin:this.originalElement.css("margin")}),this._proportionallyResize()),this.handles=n.handles||(e(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s"
,w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se");if(this.handles.constructor==String){this.handles=="all"&&(this.handles="n,e,s,w,se,sw,ne,nw");var r=this.handles.split(",");this.handles={};for(var i=0;i<r.length;i++){var s=e.trim(r[i]),o="ui-resizable-"+s,u=e('<div class="ui-resizable-handle '+o+'"></div>');u.css({zIndex:n.zIndex}),"se"==s&&u.addClass("ui-icon ui-icon-gripsmall-diagonal-se"),this.handles[s]=".ui-resizable-"+s,this.element.append(u)}}this._renderAxis=function(t){t=t||this.element;for(var n in this.handles){this.handles[n].constructor==String&&(this.handles[n]=e(this.handles[n],this.element).show());if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var r=e(this.handles[n],this.element),i=0;i=/sw|ne|nw|se|n|s/.test(n)?r.outerHeight():r.outerWidth();var s=["padding",/ne|nw|n/.test(n)?"Top":/se|sw|s/.test(n)?"Bottom":/^e$/.test(n)?"Right":"Left
"].join("");t.css(s,i),this._proportionallyResize()}if(!e(this.handles[n]).length)continue}},this._renderAxis(this.element),this._handles=e(".ui-resizable-handle",this.element).disableSelection(),this._handles.mouseover(function(){if(!t.resizing){if(this.className)var e=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);t.axis=e&&e[1]?e[1]:"se"}}),n.autoHide&&(this._handles.hide(),e(this.element).addClass("ui-resizable-autohide").mouseenter(function(){if(n.disabled)return;e(this).removeClass("ui-resizable-autohide"),t._handles.show()}).mouseleave(function(){if(n.disabled)return;t.resizing||(e(this).addClass("ui-resizable-autohide"),t._handles.hide())})),this._mouseInit()},_destroy:function(){this._mouseDestroy();var t=function(t){e(t).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").removeData("ui-resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};if(this.elementIsWrapper){t(this.element);var n=this
.element;this.originalElement.css({position:n.css("position"),width:n.outerWidth(),height:n.outerHeight(),top:n.css("top"),left:n.css("left")}).insertAfter(n),n.remove()}return this.originalElement.css("resize",this.originalResizeStyle),t(this.originalElement),this},_mouseCapture:function(t){var n=!1;for(var r in this.handles)e(this.handles[r])[0]==t.target&&(n=!0);return!this.options.disabled&&n},_mouseStart:function(t){var r=this.options,i=this.element.position(),s=this.element;this.resizing=!0,this.documentScroll={top:e(document).scrollTop(),left:e(document).scrollLeft()},(s.is(".ui-draggable")||/absolute/.test(s.css("position")))&&s.css({position:"absolute",top:i.top,left:i.left}),this._renderProxy();var o=n(this.helper.css("left")),u=n(this.helper.css("top"));r.containment&&(o+=e(r.containment).scrollLeft()||0,u+=e(r.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:o,top:u},this.size=this._helper?{width:s.outerWidth(),height:s.outerHeigh
t()}:{width:s.width(),height:s.height()},this.originalSize=this._helper?{width:s.outerWidth(),height:s.outerHeight()}:{width:s.width(),height:s.height()},this.originalPosition={left:o,top:u},this.sizeDiff={width:s.outerWidth()-s.width(),height:s.outerHeight()-s.height()},this.originalMousePosition={left:t.pageX,top:t.pageY},this.aspectRatio=typeof r.aspectRatio=="number"?r.aspectRatio:this.originalSize.width/this.originalSize.height||1;var a=e(".ui-resizable-"+this.axis).css("cursor");return e("body").css("cursor",a=="auto"?this.axis+"-resize":a),s.addClass("ui-resizable-resizing"),this._propagate("start",t),!0},_mouseDrag:function(e){var t=this.helper,n=this.options,r={},i=this,s=this.originalMousePosition,o=this.axis,u=e.pageX-s.left||0,a=e.pageY-s.top||0,f=this._change[o];if(!f)return!1;var l=f.apply(this,[e,u,a]);this._updateVirtualBoundaries(e.shiftKey);if(this._aspectRatio||e.shiftKey)l=this._updateRatio(l,e);return l=this._respectSize(l,e),this._propagate("resize",e),
t.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"}),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),this._updateCache(l),this._trigger("resize",e,this.ui()),!1},_mouseStop:function(t){this.resizing=!1;var n=this.options,r=this;if(this._helper){var i=this._proportionallyResizeElements,s=i.length&&/textarea/i.test(i[0].nodeName),o=s&&e.ui.hasScroll(i[0],"left")?0:r.sizeDiff.height,u=s?0:r.sizeDiff.width,a={width:r.helper.width()-u,height:r.helper.height()-o},f=parseInt(r.element.css("left"),10)+(r.position.left-r.originalPosition.left)||null,l=parseInt(r.element.css("top"),10)+(r.position.top-r.originalPosition.top)||null;n.animate||this.element.css(e.extend(a,{top:l,left:f})),r.helper.height(r.size.height),r.helper.width(r.size.width),this._helper&&!n.animate&&this._proportionallyResize()}return e("body").css("cursor","auto"),this.element.removeClass("ui-resizable-resiz
ing"),this._propagate("stop",t),this._helper&&this.helper.remove(),!1},_updateVirtualBoundaries:function(e){var t=this.options,n,i,s,o,u;u={minWidth:r(t.minWidth)?t.minWidth:0,maxWidth:r(t.maxWidth)?t.maxWidth:Infinity,minHeight:r(t.minHeight)?t.minHeight:0,maxHeight:r(t.maxHeight)?t.maxHeight:Infinity};if(this._aspectRatio||e)n=u.minHeight*this.aspectRatio,s=u.minWidth/this.aspectRatio,i=u.maxHeight*this.aspectRatio,o=u.maxWidth/this.aspectRatio,n>u.minWidth&&(u.minWidth=n),s>u.minHeight&&(u.minHeight=s),i<u.maxWidth&&(u.maxWidth=i),o<u.maxHeight&&(u.maxHeight=o);this._vBoundaries=u},_updateCache:function(e){var t=this.options;this.offset=this.helper.offset(),r(e.left)&&(this.position.left=e.left),r(e.top)&&(this.position.top=e.top),r(e.height)&&(this.size.height=e.height),r(e.width)&&(this.size.width=e.width)},_updateRatio:function(e,t){var n=this.options,i=this.position,s=this.size,o=this.axis;return r(e.height)?e.width=e.height*this.aspectRatio:r(e.width)&&(e.height=e.wi
dth/this.aspectRatio),o=="sw"&&(e.left=i.left+(s.width-e.width),e.top=null),o=="nw"&&(e.top=i.top+(s.height-e.height),e.left=i.left+(s.width-e.width)),e},_respectSize:function(e,t){var n=this.helper,i=this._vBoundaries,s=this._aspectRatio||t.shiftKey,o=this.axis,u=r(e.width)&&i.maxWidth&&i.maxWidth<e.width,a=r(e.height)&&i.maxHeight&&i.maxHeight<e.height,f=r(e.width)&&i.minWidth&&i.minWidth>e.width,l=r(e.height)&&i.minHeight&&i.minHeight>e.height;f&&(e.width=i.minWidth),l&&(e.height=i.minHeight),u&&(e.width=i.maxWidth),a&&(e.height=i.maxHeight);var c=this.originalPosition.left+this.originalSize.width,h=this.position.top+this.size.height,p=/sw|nw|w/.test(o),d=/nw|ne|n/.test(o);f&&p&&(e.left=c-i.minWidth),u&&p&&(e.left=c-i.maxWidth),l&&d&&(e.top=h-i.minHeight),a&&d&&(e.top=h-i.maxHeight);var v=!e.width&&!e.height;return v&&!e.left&&e.top?e.top=null:v&&!e.top&&e.left&&(e.left=null),e},_proportionallyResize:function(){var t=this.options;if(!this._proportionallyResizeElements.len
gth)return;var n=this.helper||this.element;for(var r=0;r<this._proportionallyResizeElements.length;r++){var i=this._proportionallyResizeElements[r];if(!this.borderDif){var s=[i.css("borderTopWidth"),i.css("borderRightWidth"),i.css("borderBottomWidth"),i.css("borderLeftWidth")],o=[i.css("paddingTop"),i.css("paddingRight"),i.css("paddingBottom"),i.css("paddingLeft")];this.borderDif=e.map(s,function(e,t){var n=parseInt(e,10)||0,r=parseInt(o[t],10)||0;return n+r})}i.css({height:n.height()-this.borderDif[0]-this.borderDif[2]||0,width:n.width()-this.borderDif[1]-this.borderDif[3]||0})}},_renderProxy:function(){var t=this.element,n=this.options;this.elementOffset=t.offset();if(this._helper){this.helper=this.helper||e('<div style="overflow:hidden;"></div>');var r=e.ui.ie6?1:0,i=e.ui.ie6?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+i,height:this.element.outerHeight()+i,position:"absolute",left:this.elementOffset.left-r+"px",top:this.elementOffset.top-r
+"px",zIndex:++n.zIndex}),this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(e,t,n){return{width:this.originalSize.width+t}},w:function(e,t,n){var r=this.options,i=this.originalSize,s=this.originalPosition;return{left:s.left+t,width:i.width-t}},n:function(e,t,n){var r=this.options,i=this.originalSize,s=this.originalPosition;return{top:s.top+n,height:i.height-n}},s:function(e,t,n){return{height:this.originalSize.height+n}},se:function(t,n,r){return e.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[t,n,r]))},sw:function(t,n,r){return e.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[t,n,r]))},ne:function(t,n,r){return e.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[t,n,r]))},nw:function(t,n,r){return e.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[t,n,r]))}},_propagate:function(t,n){e.ui.plugin.call(this,t,[n,this.ui()]),t!="resize"&&t
his._trigger(t,n,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),e.ui.plugin.add("resizable","alsoResize",{start:function(t,n){var r=e(this).data("resizable"),i=r.options,s=function(t){e(t).each(function(){var t=e(this);t.data("resizable-alsoresize",{width:parseInt(t.width(),10),height:parseInt(t.height(),10),left:parseInt(t.css("left"),10),top:parseInt(t.css("top"),10)})})};typeof i.alsoResize=="object"&&!i.alsoResize.parentNode?i.alsoResize.length?(i.alsoResize=i.alsoResize[0],s(i.alsoResize)):e.each(i.alsoResize,function(e){s(e)}):s(i.alsoResize)},resize:function(t,n){var r=e(this).data("resizable"),i=r.options,s=r.originalSize,o=r.originalPosition,u={height:r.size.height-s.height||0,width:r.size.width-s.width||0,top:r.position.top-o.top||0,left:r.position.left-o.left||0},a=function(t,r){e(
t).each(function(){var t=e(this),i=e(this).data("resizable-alsoresize"),s={},o=r&&r.length?r:t.parents(n.originalElement[0]).length?["width","height"]:["width","height","top","left"];e.each(o,function(e,t){var n=(i[t]||0)+(u[t]||0);n&&n>=0&&(s[t]=n||null)}),t.css(s)})};typeof i.alsoResize=="object"&&!i.alsoResize.nodeType?e.each(i.alsoResize,function(e,t){a(e,t)}):a(i.alsoResize)},stop:function(t,n){e(this).removeData("resizable-alsoresize")}}),e.ui.plugin.add("resizable","animate",{stop:function(t,n){var r=e(this).data("resizable"),i=r.options,s=r._proportionallyResizeElements,o=s.length&&/textarea/i.test(s[0].nodeName),u=o&&e.ui.hasScroll(s[0],"left")?0:r.sizeDiff.height,a=o?0:r.sizeDiff.width,f={width:r.size.width-a,height:r.size.height-u},l=parseInt(r.element.css("left"),10)+(r.position.left-r.originalPosition.left)||null,c=parseInt(r.element.css("top"),10)+(r.position.top-r.originalPosition.top)||null;r.element.animate(e.extend(f,c&&l?{top:c,left:l}:{}),{duration:i.anim
ateDuration,easing:i.animateEasing,step:function(){var n={width:parseInt(r.element.css("width"),10),height:parseInt(r.element.css("height"),10),top:parseInt(r.element.css("top"),10),left:parseInt(r.element.css("left"),10)};s&&s.length&&e(s[0]).css({width:n.width,height:n.height}),r._updateCache(n),r._propagate("resize",t)}})}}),e.ui.plugin.add("resizable","containment",{start:function(t,r){var i=e(this).data("resizable"),s=i.options,o=i.element,u=s.containment,a=u instanceof e?u.get(0):/parent/.test(u)?o.parent().get(0):u;if(!a)return;i.containerElement=e(a);if(/document/.test(u)||u==document)i.containerOffset={left:0,top:0},i.containerPosition={left:0,top:0},i.parentData={element:e(document),left:0,top:0,width:e(document).width(),height:e(document).height()||document.body.parentNode.scrollHeight};else{var f=e(a),l=[];e(["Top","Right","Left","Bottom"]).each(function(e,t){l[e]=n(f.css("padding"+t))}),i.containerOffset=f.offset(),i.containerPosition=f.position(),i.containerSiz
e={height:f.innerHeight()-l[3],width:f.innerWidth()-l[1]};var c=i.containerOffset,h=i.containerSize.height,p=i.containerSize.width,d=e.ui.hasScroll(a,"left")?a.scrollWidth:p,v=e.ui.hasScroll(a)?a.scrollHeight:h;i.parentData={element:a,left:c.left,top:c.top,width:d,height:v}}},resize:function(t,n){var r=e(this).data("resizable"),i=r.options,s=r.containerSize,o=r.containerOffset,u=r.size,a=r.position,f=r._aspectRatio||t.shiftKey,l={top:0,left:0},c=r.containerElement;c[0]!=document&&/static/.test(c.css("position"))&&(l=o),a.left<(r._helper?o.left:0)&&(r.size.width=r.size.width+(r._helper?r.position.left-o.left:r.position.left-l.left),f&&(r.size.height=r.size.width/r.aspectRatio),r.position.left=i.helper?o.left:0),a.top<(r._helper?o.top:0)&&(r.size.height=r.size.height+(r._helper?r.position.top-o.top:r.position.top),f&&(r.size.width=r.size.height*r.aspectRatio),r.position.top=r._helper?o.top:0),r.offset.left=r.parentData.left+r.position.left,r.offset.top=r.parentData.top+r.posit
ion.top;var h=Math.abs((r._helper?r.offset.left-l.left:r.offset.left-l.left)+r.sizeDiff.width),p=Math.abs((r._helper?r.offset.top-l.top:r.offset.top-o.top)+r.sizeDiff.height),d=r.containerElement.get(0)==r.element.parent().get(0),v=/relative|absolute/.test(r.containerElement.css("position"));d&&v&&(h-=r.parentData.left),h+r.size.width>=r.parentData.width&&(r.size.width=r.parentData.width-h,f&&(r.size.height=r.size.width/r.aspectRatio)),p+r.size.height>=r.parentData.height&&(r.size.height=r.parentData.height-p,f&&(r.size.width=r.size.height*r.aspectRatio))},stop:function(t,n){var r=e(this).data("resizable"),i=r.options,s=r.position,o=r.containerOffset,u=r.containerPosition,a=r.containerElement,f=e(r.helper),l=f.offset(),c=f.outerWidth()-r.sizeDiff.width,h=f.outerHeight()-r.sizeDiff.height;r._helper&&!i.animate&&/relative/.test(a.css("position"))&&e(this).css({left:l.left-u.left-o.left,width:c,height:h}),r._helper&&!i.animate&&/static/.test(a.css("position"))&&e(this).css({lef
t:l.left-u.left-o.left,width:c,height:h})}}),e.ui.plugin.add("resizable","ghost",{start:function(t,n){var r=e(this).data("resizable"),i=r.options,s=r.size;r.ghost=r.originalElement.clone(),r.ghost.css({opacity:.25,display:"block",position:"relative",height:s.height,width:s.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof i.ghost=="string"?i.ghost:""),r.ghost.appendTo(r.helper)},resize:function(t,n){var r=e(this).data("resizable"),i=r.options;r.ghost&&r.ghost.css({position:"relative",height:r.size.height,width:r.size.width})},stop:function(t,n){var r=e(this).data("resizable"),i=r.options;r.ghost&&r.helper&&r.helper.get(0).removeChild(r.ghost.get(0))}}),e.ui.plugin.add("resizable","grid",{resize:function(t,n){var r=e(this).data("resizable"),i=r.options,s=r.size,o=r.originalSize,u=r.originalPosition,a=r.axis,f=i._aspectRatio||t.shiftKey;i.grid=typeof i.grid=="number"?[i.grid,i.grid]:i.grid;var l=Math.round((s.width-o.width)/(i.grid[0]||1))*(i.grid[0]
||1),c=Math.round((s.height-o.height)/(i.grid[1]||1))*(i.grid[1]||1);/^(se|s|e)$/.test(a)?(r.size.width=o.width+l,r.size.height=o.height+c):/^(ne)$/.test(a)?(r.size.width=o.width+l,r.size.height=o.height+c,r.position.top=u.top-c):/^(sw)$/.test(a)?(r.size.width=o.width+l,r.size.height=o.height+c,r.position.left=u.left-l):(r.size.width=o.width+l,r.size.height=o.height+c,r.position.top=u.top-c,r.position.left=u.left-l)}});var n=function(e){return parseInt(e,10)||0},r=function(e){return!isNaN(parseInt(e,10))}})(jQuery);(function(e,t){var n=0,r={},i={};r.height=r.paddingTop=r.paddingBottom=r.borderTopWidth=r.borderBottomWidth="hide",i.height=i.paddingTop=i.paddingBottom=i.borderTopWidth=i.borderBottomWidth="show",e.widget("ui.accordion",{version:"1.9.2",options:{active:0,animate:{},collapsible:!1,event:"click",header:"> li > :first-child,> :not(li):even",heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null}
,_create:function(){var t=this.accordionId="ui-accordion-"+(this.element.attr("id")||++n),r=this.options;this.prevShow=this.prevHide=e(),this.element.addClass("ui-accordion ui-widget ui-helper-reset"),this.headers=this.element.find(r.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all"),this._hoverable(this.headers),this._focusable(this.headers),this.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom").hide(),!r.collapsible&&(r.active===!1||r.active==null)&&(r.active=0),r.active<0&&(r.active+=this.headers.length),this.active=this._findActive(r.active).addClass("ui-accordion-header-active ui-state-active").toggleClass("ui-corner-all ui-corner-top"),this.active.next().addClass("ui-accordion-content-active").show(),this._createIcons(),this.refresh(),this.element.attr("role","tablist"),this.headers.attr("role","tab").each(function(n){var r=e(this),i=r.attr("id"),s=r.next(),o=s.attr("id");i||(i=t+"-hea
der-"+n,r.attr("id",i)),o||(o=t+"-panel-"+n,s.attr("id",o)),r.attr("aria-controls",o),s.attr("aria-labelledby",i)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false",tabIndex:-1}).next().attr({"aria-expanded":"false","aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true",tabIndex:0}).next().attr({"aria-expanded":"true","aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._on(this.headers,{keydown:"_keydown"}),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._setupEvents(r.event)},_getCreateEventData:function(){return{header:this.active,content:this.active.length?this.active.next():e()}},_createIcons:function(){var t=this.options.icons;t&&(e("<span>").addClass("ui-accordion-header-icon ui-icon "+t.header).prependTo(this.headers),this.active.children(".ui-accordion-header-icon").removeClass(t.header).addClass(t.activeHeader),this.headers.addClass("ui-accordion-icons"))},_destr
oyIcons:function(){this.headers.removeClass("ui-accordion-icons").children(".ui-accordion-header-icon").remove()},_destroy:function(){var e;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role"),this.headers.removeClass("ui-accordion-header ui-accordion-header-active ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-selected").removeAttr("aria-controls").removeAttr("tabIndex").each(function(){/^ui-accordion/.test(this.id)&&this.removeAttribute("id")}),this._destroyIcons(),e=this.headers.next().css("display","").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-labelledby").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-state-disabled").each(function(){/^ui-accordion/.test(this.id)&&this.removeAttribute("id")}),this.options.heightStyle!=="content"&&e.css("h
eight","")},_setOption:function(e,t){if(e==="active"){this._activate(t);return}e==="event"&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(t)),this._super(e,t),e==="collapsible"&&!t&&this.options.active===!1&&this._activate(0),e==="icons"&&(this._destroyIcons(),t&&this._createIcons()),e==="disabled"&&this.headers.add(this.headers.next()).toggleClass("ui-state-disabled",!!t)},_keydown:function(t){if(t.altKey||t.ctrlKey)return;var n=e.ui.keyCode,r=this.headers.length,i=this.headers.index(t.target),s=!1;switch(t.keyCode){case n.RIGHT:case n.DOWN:s=this.headers[(i+1)%r];break;case n.LEFT:case n.UP:s=this.headers[(i-1+r)%r];break;case n.SPACE:case n.ENTER:this._eventHandler(t);break;case n.HOME:s=this.headers[0];break;case n.END:s=this.headers[r-1]}s&&(e(t.target).attr("tabIndex",-1),e(s).attr("tabIndex",0),s.focus(),t.preventDefault())},_panelKeyDown:function(t){t.keyCode===e.ui.keyCode.UP&&t.ctrlKey&&e(t.currentTarget).prev().focus()},refresh:
function(){var t,n,r=this.options.heightStyle,i=this.element.parent();r==="fill"?(e.support.minHeight||(n=i.css("overflow"),i.css("overflow","hidden")),t=i.height(),this.element.siblings(":visible").each(function(){var n=e(this),r=n.css("position");if(r==="absolute"||r==="fixed")return;t-=n.outerHeight(!0)}),n&&i.css("overflow",n),this.headers.each(function(){t-=e(this).outerHeight(!0)}),this.headers.next().each(function(){e(this).height(Math.max(0,t-e(this).innerHeight()+e(this).height()))}).css("overflow","auto")):r==="auto"&&(t=0,this.headers.next().each(function(){t=Math.max(t,e(this).css("height","").height())}).height(t))},_activate:function(t){var n=this._findActive(t)[0];if(n===this.active[0])return;n=n||this.active[0],this._eventHandler({target:n,currentTarget:n,preventDefault:e.noop})},_findActive:function(t){return typeof t=="number"?this.headers.eq(t):e()},_setupEvents:function(t){var n={};if(!t)return;e.each(t.split(" "),function(e,t){n[t]="_eventHandler"}),this
._on(this.headers,n)},_eventHandler:function(t){var n=this.options,r=this.active,i=e(t.currentTarget),s=i[0]===r[0],o=s&&n.collapsible,u=o?e():i.next(),a=r.next(),f={oldHeader:r,oldPanel:a,newHeader:o?e():i,newPanel:u};t.preventDefault();if(s&&!n.collapsible||this._trigger("beforeActivate",t,f)===!1)return;n.active=o?!1:this.headers.index(i),this.active=s?e():i,this._toggle(f),r.removeClass("ui-accordion-header-active ui-state-active"),n.icons&&r.children(".ui-accordion-header-icon").removeClass(n.icons.activeHeader).addClass(n.icons.header),s||(i.removeClass("ui-corner-all").addClass("ui-accordion-header-active ui-state-active ui-corner-top"),n.icons&&i.children(".ui-accordion-header-icon").removeClass(n.icons.header).addClass(n.icons.activeHeader),i.next().addClass("ui-accordion-content-active"))},_toggle:function(t){var n=t.newPanel,r=this.prevShow.length?this.prevShow:t.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=n,this.prevHide=r,this.options.ani
mate?this._animate(n,r,t):(r.hide(),n.show(),this._toggleComplete(t)),r.attr({"aria-expanded":"false","aria-hidden":"true"}),r.prev().attr("aria-selected","false"),n.length&&r.length?r.prev().attr("tabIndex",-1):n.length&&this.headers.filter(function(){return e(this).attr("tabIndex")===0}).attr("tabIndex",-1),n.attr({"aria-expanded":"true","aria-hidden":"false"}).prev().attr({"aria-selected":"true",tabIndex:0})},_animate:function(e,t,n){var s,o,u,a=this,f=0,l=e.length&&(!t.length||e.index()<t.index()),c=this.options.animate||{},h=l&&c.down||c,p=function(){a._toggleComplete(n)};typeof h=="number"&&(u=h),typeof h=="string"&&(o=h),o=o||h.easing||c.easing,u=u||h.duration||c.duration;if(!t.length)return e.animate(i,u,o,p);if(!e.length)return t.animate(r,u,o,p);s=e.show().outerHeight(),t.animate(r,{duration:u,easing:o,step:function(e,t){t.now=Math.round(e)}}),e.hide().animate(i,{duration:u,easing:o,complete:p,step:function(e,n){n.now=Math.round(e),n.prop!=="height"?f+=n.now:a.opti
ons.heightStyle!=="content"&&(n.now=Math.round(s-t.outerHeight()-f),f=0)}})},_toggleComplete:function(e){var t=e.oldPanel;t.removeClass("ui-accordion-content-active").prev().removeClass("ui-corner-top").addClass("ui-corner-all"),t.length&&(t.parent()[0].className=t.parent()[0].className),this._trigger("activate",null,e)}}),e.uiBackCompat!==!1&&(function(e,t){e.extend(t.options,{navigation:!1,navigationFilter:function(){return this.href.toLowerCase()===location.href.toLowerCase()}});var n=t._create;t._create=function(){if(this.options.navigation){var t=this,r=this.element.find(this.options.header),i=r.next(),s=r.add(i).find("a").filter(this.options.navigationFilter)[0];s&&r.add(i).each(function(n){if(e.contains(this,s))return t.options.active=Math.floor(n/2),!1})}n.call(this)}}(jQuery,jQuery.ui.accordion.prototype),function(e,t){e.extend(t.options,{heightStyle:null,autoHeight:!0,clearStyle:!1,fillSpace:!1});var n=t._create,r=t._setOption;e.extend(t,{_create:function(){this.op
tions.heightStyle=this.options.heightStyle||this._mergeHeightStyle(),n.call(this)},_setOption:function(e){if(e==="autoHeight"||e==="clearStyle"||e==="fillSpace")this.options.heightStyle=this._mergeHeightStyle();r.apply(this,arguments)},_mergeHeightStyle:function(){var e=this.options;if(e.fillSpace)return"fill";if(e.clearStyle)return"content";if(e.autoHeight)return"auto"}})}(jQuery,jQuery.ui.accordion.prototype),function(e,t){e.extend(t.options.icons,{activeHeader:null,headerSelected:"ui-icon-triangle-1-s"});var n=t._createIcons;t._createIcons=function(){this.options.icons&&(this.options.icons.activeHeader=this.options.icons.activeHeader||this.options.icons.headerSelected),n.call(this)}}(jQuery,jQuery.ui.accordion.prototype),function(e,t){t.activate=t._activate;var n=t._findActive;t._findActive=function(e){return e===-1&&(e=!1),e&&typeof e!="number"&&(e=this.headers.index(this.headers.filter(e)),e===-1&&(e=!1)),n.call(this,e)}}(jQuery,jQuery.ui.accordion.prototype),jQuery.ui.
accordion.prototype.resize=jQuery.ui.accordion.prototype.refresh,function(e,t){e.extend(t.options,{change:null,changestart:null});var n=t._trigger;t._trigger=function(e,t,r){var i=n.apply(this,arguments);return i?(e==="beforeActivate"?i=n.call(this,"changestart",t,{oldHeader:r.oldHeader,oldContent:r.oldPanel,newHeader:r.newHeader,newContent:r.newPanel}):e==="activate"&&(i=n.call(this,"change",t,{oldHeader:r.oldHeader,oldContent:r.oldPanel,newHeader:r.newHeader,newContent:r.newPanel})),i):!1}}(jQuery,jQuery.ui.accordion.prototype),function(e,t){e.extend(t.options,{animate:null,animated:"slide"});var n=t._create;t._create=function(){var e=this.options;e.animate===null&&(e.animated?e.animated==="slide"?e.animate=300:e.animated==="bounceslide"?e.animate={duration:200,down:{easing:"easeOutBounce",duration:1e3}}:e.animate=e.animated:e.animate=!1),n.call(this)}}(jQuery,jQuery.ui.accordion.prototype))})(jQuery);(function(e,t){var n,r,i,s,o="ui-button ui-widget ui-state-default ui-co
rner-all",u="ui-state-hover ui-state-active ",a="ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only",f=function(){var t=e(this).find(":ui-button");setTimeout(function(){t.button("refresh")},1)},l=function(t){var n=t.name,r=t.form,i=e([]);return n&&(r?i=e(r).find("[name='"+n+"']"):i=e("[name='"+n+"']",t.ownerDocument).filter(function(){return!this.form})),i};e.widget("ui.button",{version:"1.9.2",defaultElement:"<button>",options:{disabled:null,text:!0,label:null,icons:{primary:null,secondary:null}},_create:function(){this.element.closest("form").unbind("reset"+this.eventNamespace).bind("reset"+this.eventNamespace,f),typeof this.options.disabled!="boolean"?this.options.disabled=!!this.element.prop("disabled"):this.element.prop("disabled",this.options.disabled),this._determineButtonType(),this.hasTitle=!!this.buttonElement.attr("title");var t=this,u=this.options,a=this.type==="checkbox"||th
is.type==="radio",c=a?"":"ui-state-active",h="ui-state-focus";u.label===null&&(u.label=this.type==="input"?this.buttonElement.val():this.buttonElement.html()),this._hoverable(this.buttonElement),this.buttonElement.addClass(o).attr("role","button").bind("mouseenter"+this.eventNamespace,function(){if(u.disabled)return;this===n&&e(this).addClass("ui-state-active")}).bind("mouseleave"+this.eventNamespace,function(){if(u.disabled)return;e(this).removeClass(c)}).bind("click"+this.eventNamespace,function(e){u.disabled&&(e.preventDefault(),e.stopImmediatePropagation())}),this.element.bind("focus"+this.eventNamespace,function(){t.buttonElement.addClass(h)}).bind("blur"+this.eventNamespace,function(){t.buttonElement.removeClass(h)}),a&&(this.element.bind("change"+this.eventNamespace,function(){if(s)return;t.refresh()}),this.buttonElement.bind("mousedown"+this.eventNamespace,function(e){if(u.disabled)return;s=!1,r=e.pageX,i=e.pageY}).bind("mouseup"+this.eventNamespace,function(e){if(u.
disabled)return;if(r!==e.pageX||i!==e.pageY)s=!0})),this.type==="checkbox"?this.buttonElement.bind("click"+this.eventNamespace,function(){if(u.disabled||s)return!1;e(this).toggleClass("ui-state-active"),t.buttonElement.attr("aria-pressed",t.element[0].checked)}):this.type==="radio"?this.buttonElement.bind("click"+this.eventNamespace,function(){if(u.disabled||s)return!1;e(this).addClass("ui-state-active"),t.buttonElement.attr("aria-pressed","true");var n=t.element[0];l(n).not(n).map(function(){return e(this).button("widget")[0]}).removeClass("ui-state-active").attr("aria-pressed","false")}):(this.buttonElement.bind("mousedown"+this.eventNamespace,function(){if(u.disabled)return!1;e(this).addClass("ui-state-active"),n=this,t.document.one("mouseup",function(){n=null})}).bind("mouseup"+this.eventNamespace,function(){if(u.disabled)return!1;e(this).removeClass("ui-state-active")}).bind("keydown"+this.eventNamespace,function(t){if(u.disabled)return!1;(t.keyCode===e.ui.keyCode.SPACE
||t.keyCode===e.ui.keyCode.ENTER)&&e(this).addClass("ui-state-active")}).bind("keyup"+this.eventNamespace,function(){e(this).removeClass("ui-state-active")}),this.buttonElement.is("a")&&this.buttonElement.keyup(function(t){t.keyCode===e.ui.keyCode.SPACE&&e(this).click()})),this._setOption("disabled",u.disabled),this._resetButton()},_determineButtonType:function(){var e,t,n;this.element.is("[type=checkbox]")?this.type="checkbox":this.element.is("[type=radio]")?this.type="radio":this.element.is("input")?this.type="input":this.type="button",this.type==="checkbox"||this.type==="radio"?(e=this.element.parents().last(),t="label[for='"+this.element.attr("id")+"']",this.buttonElement=e.find(t),this.buttonElement.length||(e=e.length?e.siblings():this.element.siblings(),this.buttonElement=e.filter(t),this.buttonElement.length||(this.buttonElement=e.find(t))),this.element.addClass("ui-helper-hidden-accessible"),n=this.element.is(":checked"),n&&this.buttonElement.addClass("ui-state-acti
ve"),this.buttonElement.prop("aria-pressed",n)):this.buttonElement=this.element},widget:function(){return this.buttonElement},_destroy:function(){this.element.removeClass("ui-helper-hidden-accessible"),this.buttonElement.removeClass(o+" "+u+" "+a).removeAttr("role").removeAttr("aria-pressed").html(this.buttonElement.find(".ui-button-text").html()),this.hasTitle||this.buttonElement.removeAttr("title")},_setOption:function(e,t){this._super(e,t);if(e==="disabled"){t?this.element.prop("disabled",!0):this.element.prop("disabled",!1);return}this._resetButton()},refresh:function(){var t=this.element.is("input, button")?this.element.is(":disabled"):this.element.hasClass("ui-button-disabled");t!==this.options.disabled&&this._setOption("disabled",t),this.type==="radio"?l(this.element[0]).each(function(){e(this).is(":checked")?e(this).button("widget").addClass("ui-state-active").attr("aria-pressed","true"):e(this).button("widget").removeClass("ui-state-active").attr("aria-pressed","fal
se")}):this.type==="checkbox"&&(this.element.is(":checked")?this.buttonElement.addClass("ui-state-active").attr("aria-pressed","true"):this.buttonElement.removeClass("ui-state-active").attr("aria-pressed","false"))},_resetButton:function(){if(this.type==="input"){this.options.label&&this.element.val(this.options.label);return}var t=this.buttonElement.removeClass(a),n=e("<span></span>",this.document[0]).addClass("ui-button-text").html(this.options.label).appendTo(t.empty()).text(),r=this.options.icons,i=r.primary&&r.secondary,s=[];r.primary||r.secondary?(this.options.text&&s.push("ui-button-text-icon"+(i?"s":r.primary?"-primary":"-secondary")),r.primary&&t.prepend("<span class='ui-button-icon-primary ui-icon "+r.primary+"'></span>"),r.secondary&&t.append("<span class='ui-button-icon-secondary ui-icon "+r.secondary+"'></span>"),this.options.text||(s.push(i?"ui-button-icons-only":"ui-button-icon-only"),this.hasTitle||t.attr("title",e.trim(n)))):s.push("ui-button-text-only"),t.a
ddClass(s.join(" "))}}),e.widget("ui.buttonset",{version:"1.9.2",options:{items:"button, input[type=button], input[type=submit], input[type=reset], input[type=checkbox], input[type=radio], a, :data(button)"},_create:function(){this.element.addClass("ui-buttonset")},_init:function(){this.refresh()},_setOption:function(e,t){e==="disabled"&&this.buttons.button("option",e,t),this._super(e,t)},refresh:function(){var t=this.element.css("direction")==="rtl";this.buttons=this.element.find(this.options.items).filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return e(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":first").addClass(t?"ui-corner-right":"ui-corner-left").end().filter(":last").addClass(t?"ui-corner-left":"ui-corner-right").end().end()},_destroy:function(){this.element.removeClass("ui-buttonset"),this.buttons.map(function(){return e(this).button("widget")[0]}).removeClass("ui-corne
r-left ui-corner-right").end().button("destroy")}})})(jQuery);(function($,undefined){function Datepicker(){this.debug=!1,this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday
","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},$.extend(this._defaults,this.regional[""]),this.dpDiv=bindHover($('<div id="'+this._mainDivId+'" class="ui-dat
epicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>'))}function bindHover(e){var t="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return e.delegate(t,"mouseout",function(){$(this).removeClass("ui-state-hover"),this.className.indexOf("ui-datepicker-prev")!=-1&&$(this).removeClass("ui-datepicker-prev-hover"),this.className.indexOf("ui-datepicker-next")!=-1&&$(this).removeClass("ui-datepicker-next-hover")}).delegate(t,"mouseover",function(){$.datepicker._isDisabledDatepicker(instActive.inline?e.parent()[0]:instActive.input[0])||($(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),$(this).addClass("ui-state-hover"),this.className.indexOf("ui-datepicker-prev")!=-1&&$(this).addClass("ui-datepicker-prev-hover"),this.className.indexOf("ui-datepicker-next")!=-1&&$(this).addClass("ui-datepicker-next-hover"))})}function extendRemove(e,t){$.extend(e,t);for(var n in t)if(t[n]==null||t[n]==undefined)
e[n]=t[n];return e}$.extend($.ui,{datepicker:{version:"1.9.2"}});var PROP_NAME="datepicker",dpuuid=(new Date).getTime(),instActive;$.extend(Datepicker.prototype,{markerClassName:"hasDatepicker",maxRows:4,log:function(){this.debug&&console.log.apply("",arguments)},_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(e){return extendRemove(this._defaults,e||{}),this},_attachDatepicker:function(target,settings){var inlineSettings=null;for(var attrName in this._defaults){var attrValue=target.getAttribute("date:"+attrName);if(attrValue){inlineSettings=inlineSettings||{};try{inlineSettings[attrName]=eval(attrValue)}catch(err){inlineSettings[attrName]=attrValue}}}var nodeName=target.nodeName.toLowerCase(),inline=nodeName=="div"||nodeName=="span";target.id||(this.uuid+=1,target.id="dp"+this.uuid);var inst=this._newInst($(target),inline);inst.settings=$.extend({},settings||{},inlineSettings||{}),nodeName=="input"?this._connectDatepicker(target,inst):inline&&this._inli
neDatepicker(target,inst)},_newInst:function(e,t){var n=e[0].id.replace(/([^A-Za-z0-9_-])/g,"\\\\$1");return{id:n,input:e,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:t,dpDiv:t?bindHover($('<div class="'+this._inlineClass+' ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>')):this.dpDiv}},_connectDatepicker:function(e,t){var n=$(e);t.append=$([]),t.trigger=$([]);if(n.hasClass(this.markerClassName))return;this._attachments(n,t),n.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker",function(e,n,r){t.settings[n]=r}).bind("getData.datepicker",function(e,n){return this._get(t,n)}),this._autoSize(t),$.data(e,PROP_NAME,t),t.settings.disabled&&this._disableDatepicker(e)},_attachments:function(e,t){var n=this._get(t,"appendText"),r=this._get(t,"isRTL");t.append&&t.append.remove(),n&&(t.append=$('<span class="'+this._appendClass+'">'+n+"</span>")
,e[r?"before":"after"](t.append)),e.unbind("focus",this._showDatepicker),t.trigger&&t.trigger.remove();var i=this._get(t,"showOn");(i=="focus"||i=="both")&&e.focus(this._showDatepicker);if(i=="button"||i=="both"){var s=this._get(t,"buttonText"),o=this._get(t,"buttonImage");t.trigger=$(this._get(t,"buttonImageOnly")?$("<img/>").addClass(this._triggerClass).attr({src:o,alt:s,title:s}):$('<button type="button"></button>').addClass(this._triggerClass).html(o==""?s:$("<img/>").attr({src:o,alt:s,title:s}))),e[r?"before":"after"](t.trigger),t.trigger.click(function(){return $.datepicker._datepickerShowing&&$.datepicker._lastInput==e[0]?$.datepicker._hideDatepicker():$.datepicker._datepickerShowing&&$.datepicker._lastInput!=e[0]?($.datepicker._hideDatepicker(),$.datepicker._showDatepicker(e[0])):$.datepicker._showDatepicker(e[0]),!1})}},_autoSize:function(e){if(this._get(e,"autoSize")&&!e.inline){var t=new Date(2009,11,20),n=this._get(e,"dateFormat");if(n.match(/[DM]/)){var r=functi
on(e){var t=0,n=0;for(var r=0;r<e.length;r++)e[r].length>t&&(t=e[r].length,n=r);return n};t.setMonth(r(this._get(e,n.match(/MM/)?"monthNames":"monthNamesShort"))),t.setDate(r(this._get(e,n.match(/DD/)?"dayNames":"dayNamesShort"))+20-t.getDay())}e.input.attr("size",this._formatDate(e,t).length)}},_inlineDatepicker:function(e,t){var n=$(e);if(n.hasClass(this.markerClassName))return;n.addClass(this.markerClassName).append(t.dpDiv).bind("setData.datepicker",function(e,n,r){t.settings[n]=r}).bind("getData.datepicker",function(e,n){return this._get(t,n)}),$.data(e,PROP_NAME,t),this._setDate(t,this._getDefaultDate(t),!0),this._updateDatepicker(t),this._updateAlternate(t),t.settings.disabled&&this._disableDatepicker(e),t.dpDiv.css("display","block")},_dialogDatepicker:function(e,t,n,r,i){var s=this._dialogInst;if(!s){this.uuid+=1;var o="dp"+this.uuid;this._dialogInput=$('<input type="text" id="'+o+'" style="position: absolute; top: -100px; width: 0px;"/>'),this._dialogInput.keydown(
this._doKeyDown),$("body").append(this._dialogInput),s=this._dialogInst=this._newInst(this._dialogInput,!1),s.settings={},$.data(this._dialogInput[0],PROP_NAME,s)}extendRemove(s.settings,r||{}),t=t&&t.constructor==Date?this._formatDate(s,t):t,this._dialogInput.val(t),this._pos=i?i.length?i:[i.pageX,i.pageY]:null;if(!this._pos){var u=document.documentElement.clientWidth,a=document.documentElement.clientHeight,f=document.documentElement.scrollLeft||document.body.scrollLeft,l=document.documentElement.scrollTop||document.body.scrollTop;this._pos=[u/2-100+f,a/2-150+l]}return this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),s.settings.onSelect=n,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),$.blockUI&&$.blockUI(this.dpDiv),$.data(this._dialogInput[0],PROP_NAME,s),this},_destroyDatepicker:function(e){var t=$(e),n=$.data(e,PROP_NAME);if(!t.hasClass(this.markerClassName))return;var r=e.nodeName.toLow
erCase();$.removeData(e,PROP_NAME),r=="input"?(n.append.remove(),n.trigger.remove(),t.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)):(r=="div"||r=="span")&&t.removeClass(this.markerClassName).empty()},_enableDatepicker:function(e){var t=$(e),n=$.data(e,PROP_NAME);if(!t.hasClass(this.markerClassName))return;var r=e.nodeName.toLowerCase();if(r=="input")e.disabled=!1,n.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""});else if(r=="div"||r=="span"){var i=t.children("."+this._inlineClass);i.children().removeClass("ui-state-disabled"),i.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!1)}this._disabledInputs=$.map(this._disabledInputs,function(t){return t==e?null:t})},_disableDatepicker:function(e){var t=$(e),n=$.data(e,PROP_NAME);if(!t.hasClass(this.markerClassName))re
turn;var r=e.nodeName.toLowerCase();if(r=="input")e.disabled=!0,n.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"});else if(r=="div"||r=="span"){var i=t.children("."+this._inlineClass);i.children().addClass("ui-state-disabled"),i.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!0)}this._disabledInputs=$.map(this._disabledInputs,function(t){return t==e?null:t}),this._disabledInputs[this._disabledInputs.length]=e},_isDisabledDatepicker:function(e){if(!e)return!1;for(var t=0;t<this._disabledInputs.length;t++)if(this._disabledInputs[t]==e)return!0;return!1},_getInst:function(e){try{return $.data(e,PROP_NAME)}catch(t){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(e,t,n){var r=this._getInst(e);if(arguments.length==2&&typeof t=="string")return t=="defaults"?$.extend({},$.datepicker._defaults):r?t=="all"?$.extend({},r.settings):this._get(r,t):null;var i=t
||{};typeof t=="string"&&(i={},i[t]=n);if(r){this._curInst==r&&this._hideDatepicker();var s=this._getDateDatepicker(e,!0),o=this._getMinMaxDate(r,"min"),u=this._getMinMaxDate(r,"max");extendRemove(r.settings,i),o!==null&&i.dateFormat!==undefined&&i.minDate===undefined&&(r.settings.minDate=this._formatDate(r,o)),u!==null&&i.dateFormat!==undefined&&i.maxDate===undefined&&(r.settings.maxDate=this._formatDate(r,u)),this._attachments($(e),r),this._autoSize(r),this._setDate(r,s),this._updateAlternate(r),this._updateDatepicker(r)}},_changeDatepicker:function(e,t,n){this._optionDatepicker(e,t,n)},_refreshDatepicker:function(e){var t=this._getInst(e);t&&this._updateDatepicker(t)},_setDateDatepicker:function(e,t){var n=this._getInst(e);n&&(this._setDate(n,t),this._updateDatepicker(n),this._updateAlternate(n))},_getDateDatepicker:function(e,t){var n=this._getInst(e);return n&&!n.inline&&this._setDateFromField(n,t),n?this._getDate(n):null},_doKeyDown:function(e){var t=$.datepicker._getI
nst(e.target),n=!0,r=t.dpDiv.is(".ui-datepicker-rtl");t._keyEvent=!0;if($.datepicker._datepickerShowing)switch(e.keyCode){case 9:$.datepicker._hideDatepicker(),n=!1;break;case 13:var i=$("td."+$.datepicker._dayOverClass+":not(."+$.datepicker._currentClass+")",t.dpDiv);i[0]&&$.datepicker._selectDay(e.target,t.selectedMonth,t.selectedYear,i[0]);var s=$.datepicker._get(t,"onSelect");if(s){var o=$.datepicker._formatDate(t);s.apply(t.input?t.input[0]:null,[o,t])}else $.datepicker._hideDatepicker();return!1;case 27:$.datepicker._hideDatepicker();break;case 33:$.datepicker._adjustDate(e.target,e.ctrlKey?-$.datepicker._get(t,"stepBigMonths"):-$.datepicker._get(t,"stepMonths"),"M");break;case 34:$.datepicker._adjustDate(e.target,e.ctrlKey?+$.datepicker._get(t,"stepBigMonths"):+$.datepicker._get(t,"stepMonths"),"M");break;case 35:(e.ctrlKey||e.metaKey)&&$.datepicker._clearDate(e.target),n=e.ctrlKey||e.metaKey;break;case 36:(e.ctrlKey||e.metaKey)&&$.datepicker._gotoToday(e.target),n=e.
ctrlKey||e.metaKey;break;case 37:(e.ctrlKey||e.metaKey)&&$.datepicker._adjustDate(e.target,r?1:-1,"D"),n=e.ctrlKey||e.metaKey,e.originalEvent.altKey&&$.datepicker._adjustDate(e.target,e.ctrlKey?-$.datepicker._get(t,"stepBigMonths"):-$.datepicker._get(t,"stepMonths"),"M");break;case 38:(e.ctrlKey||e.metaKey)&&$.datepicker._adjustDate(e.target,-7,"D"),n=e.ctrlKey||e.metaKey;break;case 39:(e.ctrlKey||e.metaKey)&&$.datepicker._adjustDate(e.target,r?-1:1,"D"),n=e.ctrlKey||e.metaKey,e.originalEvent.altKey&&$.datepicker._adjustDate(e.target,e.ctrlKey?+$.datepicker._get(t,"stepBigMonths"):+$.datepicker._get(t,"stepMonths"),"M");break;case 40:(e.ctrlKey||e.metaKey)&&$.datepicker._adjustDate(e.target,7,"D"),n=e.ctrlKey||e.metaKey;break;default:n=!1}else e.keyCode==36&&e.ctrlKey?$.datepicker._showDatepicker(this):n=!1;n&&(e.preventDefault(),e.stopPropagation())},_doKeyPress:function(e){var t=$.datepicker._getInst(e.target);if($.datepicker._get(t,"constrainInput")){var n=$.datepicker._p
ossibleChars($.datepicker._get(t,"dateFormat")),r=String.fromCharCode(e.charCode==undefined?e.keyCode:e.charCode);return e.ctrlKey||e.metaKey||r<" "||!n||n.indexOf(r)>-1}},_doKeyUp:function(e){var t=$.datepicker._getInst(e.target);if(t.input.val()!=t.lastVal)try{var n=$.datepicker.parseDate($.datepicker._get(t,"dateFormat"),t.input?t.input.val():null,$.datepicker._getFormatConfig(t));n&&($.datepicker._setDateFromField(t),$.datepicker._updateAlternate(t),$.datepicker._updateDatepicker(t))}catch(r){$.datepicker.log(r)}return!0},_showDatepicker:function(e){e=e.target||e,e.nodeName.toLowerCase()!="input"&&(e=$("input",e.parentNode)[0]);if($.datepicker._isDisabledDatepicker(e)||$.datepicker._lastInput==e)return;var t=$.datepicker._getInst(e);$.datepicker._curInst&&$.datepicker._curInst!=t&&($.datepicker._curInst.dpDiv.stop(!0,!0),t&&$.datepicker._datepickerShowing&&$.datepicker._hideDatepicker($.datepicker._curInst.input[0]));var n=$.datepicker._get(t,"beforeShow"),r=n?n.apply(e,
[e,t]):{};if(r===!1)return;extendRemove(t.settings,r),t.lastVal=null,$.datepicker._lastInput=e,$.datepicker._setDateFromField(t),$.datepicker._inDialog&&(e.value=""),$.datepicker._pos||($.datepicker._pos=$.datepicker._findPos(e),$.datepicker._pos[1]+=e.offsetHeight);var i=!1;$(e).parents().each(function(){return i|=$(this).css("position")=="fixed",!i});var s={left:$.datepicker._pos[0],top:$.datepicker._pos[1]};$.datepicker._pos=null,t.dpDiv.empty(),t.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),$.datepicker._updateDatepicker(t),s=$.datepicker._checkOffset(t,s,i),t.dpDiv.css({position:$.datepicker._inDialog&&$.blockUI?"static":i?"fixed":"absolute",display:"none",left:s.left+"px",top:s.top+"px"});if(!t.inline){var o=$.datepicker._get(t,"showAnim"),u=$.datepicker._get(t,"duration"),a=function(){var e=t.dpDiv.find("iframe.ui-datepicker-cover");if(!!e.length){var n=$.datepicker._getBorders(t.dpDiv);e.css({left:-n[0],top:-n[1],width:t.dpDiv.outerWidth(),height:t.
dpDiv.outerHeight()})}};t.dpDiv.zIndex($(e).zIndex()+1),$.datepicker._datepickerShowing=!0,$.effects&&($.effects.effect[o]||$.effects[o])?t.dpDiv.show(o,$.datepicker._get(t,"showOptions"),u,a):t.dpDiv[o||"show"](o?u:null,a),(!o||!u)&&a(),t.input.is(":visible")&&!t.input.is(":disabled")&&t.input.focus(),$.datepicker._curInst=t}},_updateDatepicker:function(e){this.maxRows=4;var t=$.datepicker._getBorders(e.dpDiv);instActive=e,e.dpDiv.empty().append(this._generateHTML(e)),this._attachHandlers(e);var n=e.dpDiv.find("iframe.ui-datepicker-cover");!n.length||n.css({left:-t[0],top:-t[1],width:e.dpDiv.outerWidth(),height:e.dpDiv.outerHeight()}),e.dpDiv.find("."+this._dayOverClass+" a").mouseover();var r=this._getNumberOfMonths(e),i=r[1],s=17;e.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),i>1&&e.dpDiv.addClass("ui-datepicker-multi-"+i).css("width",s*i+"em"),e.dpDiv[(r[0]!=1||r[1]!=1?"add":"remove")+"Class"]("ui-datepicker-multi"),e.d
pDiv[(this._get(e,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),e==$.datepicker._curInst&&$.datepicker._datepickerShowing&&e.input&&e.input.is(":visible")&&!e.input.is(":disabled")&&e.input[0]!=document.activeElement&&e.input.focus();if(e.yearshtml){var o=e.yearshtml;setTimeout(function(){o===e.yearshtml&&e.yearshtml&&e.dpDiv.find("select.ui-datepicker-year:first").replaceWith(e.yearshtml),o=e.yearshtml=null},0)}},_getBorders:function(e){var t=function(e){return{thin:1,medium:2,thick:3}[e]||e};return[parseFloat(t(e.css("border-left-width"))),parseFloat(t(e.css("border-top-width")))]},_checkOffset:function(e,t,n){var r=e.dpDiv.outerWidth(),i=e.dpDiv.outerHeight(),s=e.input?e.input.outerWidth():0,o=e.input?e.input.outerHeight():0,u=document.documentElement.clientWidth+(n?0:$(document).scrollLeft()),a=document.documentElement.clientHeight+(n?0:$(document).scrollTop());return t.left-=this._get(e,"isRTL")?r-s:0,t.left-=n&&t.left==e.input.offset().left?$(document).scrollL
eft():0,t.top-=n&&t.top==e.input.offset().top+o?$(document).scrollTop():0,t.left-=Math.min(t.left,t.left+r>u&&u>r?Math.abs(t.left+r-u):0),t.top-=Math.min(t.top,t.top+i>a&&a>i?Math.abs(i+o):0),t},_findPos:function(e){var t=this._getInst(e),n=this._get(t,"isRTL");while(e&&(e.type=="hidden"||e.nodeType!=1||$.expr.filters.hidden(e)))e=e[n?"previousSibling":"nextSibling"];var r=$(e).offset();return[r.left,r.top]},_hideDatepicker:function(e){var t=this._curInst;if(!t||e&&t!=$.data(e,PROP_NAME))return;if(this._datepickerShowing){var n=this._get(t,"showAnim"),r=this._get(t,"duration"),i=function(){$.datepicker._tidyDialog(t)};$.effects&&($.effects.effect[n]||$.effects[n])?t.dpDiv.hide(n,$.datepicker._get(t,"showOptions"),r,i):t.dpDiv[n=="slideDown"?"slideUp":n=="fadeIn"?"fadeOut":"hide"](n?r:null,i),n||i(),this._datepickerShowing=!1;var s=this._get(t,"onClose");s&&s.apply(t.input?t.input[0]:null,[t.input?t.input.val():"",t]),this._lastInput=null,this._inDialog&&(this._dialogInput.cs
s({position:"absolute",left:"0",top:"-100px"}),$.blockUI&&($.unblockUI(),$("body").append(this.dpDiv))),this._inDialog=!1}},_tidyDialog:function(e){e.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(e){if(!$.datepicker._curInst)return;var t=$(e.target),n=$.datepicker._getInst(t[0]);(t[0].id!=$.datepicker._mainDivId&&t.parents("#"+$.datepicker._mainDivId).length==0&&!t.hasClass($.datepicker.markerClassName)&&!t.closest("."+$.datepicker._triggerClass).length&&$.datepicker._datepickerShowing&&(!$.datepicker._inDialog||!$.blockUI)||t.hasClass($.datepicker.markerClassName)&&$.datepicker._curInst!=n)&&$.datepicker._hideDatepicker()},_adjustDate:function(e,t,n){var r=$(e),i=this._getInst(r[0]);if(this._isDisabledDatepicker(r[0]))return;this._adjustInstDate(i,t+(n=="M"?this._get(i,"showCurrentAtPos"):0),n),this._updateDatepicker(i)},_gotoToday:function(e){var t=$(e),n=this._getInst(t[0]);if(this._get(n,"gotoCurrent")&&n.currentDay)
n.selectedDay=n.currentDay,n.drawMonth=n.selectedMonth=n.currentMonth,n.drawYear=n.selectedYear=n.currentYear;else{var r=new Date;n.selectedDay=r.getDate(),n.drawMonth=n.selectedMonth=r.getMonth(),n.drawYear=n.selectedYear=r.getFullYear()}this._notifyChange(n),this._adjustDate(t)},_selectMonthYear:function(e,t,n){var r=$(e),i=this._getInst(r[0]);i["selected"+(n=="M"?"Month":"Year")]=i["draw"+(n=="M"?"Month":"Year")]=parseInt(t.options[t.selectedIndex].value,10),this._notifyChange(i),this._adjustDate(r)},_selectDay:function(e,t,n,r){var i=$(e);if($(r).hasClass(this._unselectableClass)||this._isDisabledDatepicker(i[0]))return;var s=this._getInst(i[0]);s.selectedDay=s.currentDay=$("a",r).html(),s.selectedMonth=s.currentMonth=t,s.selectedYear=s.currentYear=n,this._selectDate(e,this._formatDate(s,s.currentDay,s.currentMonth,s.currentYear))},_clearDate:function(e){var t=$(e),n=this._getInst(t[0]);this._selectDate(t,"")},_selectDate:function(e,t){var n=$(e),r=this._getInst(n[0]);t=
t!=null?t:this._formatDate(r),r.input&&r.input.val(t),this._updateAlternate(r);var i=this._get(r,"onSelect");i?i.apply(r.input?r.input[0]:null,[t,r]):r.input&&r.input.trigger("change"),r.inline?this._updateDatepicker(r):(this._hideDatepicker(),this._lastInput=r.input[0],typeof r.input[0]!="object"&&r.input.focus(),this._lastInput=null)},_updateAlternate:function(e){var t=this._get(e,"altField");if(t){var n=this._get(e,"altFormat")||this._get(e,"dateFormat"),r=this._getDate(e),i=this.formatDate(n,r,this._getFormatConfig(e));$(t).each(function(){$(this).val(i)})}},noWeekends:function(e){var t=e.getDay();return[t>0&&t<6,""]},iso8601Week:function(e){var t=new Date(e.getTime());t.setDate(t.getDate()+4-(t.getDay()||7));var n=t.getTime();return t.setMonth(0),t.setDate(1),Math.floor(Math.round((n-t)/864e5)/7)+1},parseDate:function(e,t,n){if(e==null||t==null)throw"Invalid arguments";t=typeof t=="object"?t.toString():t+"";if(t=="")return null;var r=(n?n.shortYearCutoff:null)||this._de
faults.shortYearCutoff;r=typeof r!="string"?r:(new Date).getFullYear()%100+parseInt(r,10);var i=(n?n.dayNamesShort:null)||this._defaults.dayNamesShort,s=(n?n.dayNames:null)||this._defaults.dayNames,o=(n?n.monthNamesShort:null)||this._defaults.monthNamesShort,u=(n?n.monthNames:null)||this._defaults.monthNames,a=-1,f=-1,l=-1,c=-1,h=!1,p=function(t){var n=y+1<e.length&&e.charAt(y+1)==t;return n&&y++,n},d=function(e){var n=p(e),r=e=="@"?14:e=="!"?20:e=="y"&&n?4:e=="o"?3:2,i=new RegExp("^\\d{1,"+r+"}"),s=t.substring(g).match(i);if(!s)throw"Missing number at position "+g;return g+=s[0].length,parseInt(s[0],10)},v=function(e,n,r){var i=$.map(p(e)?r:n,function(e,t){return[[t,e]]}).sort(function(e,t){return-(e[1].length-t[1].length)}),s=-1;$.each(i,function(e,n){var r=n[1];if(t.substr(g,r.length).toLowerCase()==r.toLowerCase())return s=n[0],g+=r.length,!1});if(s!=-1)return s+1;throw"Unknown name at position "+g},m=function(){if(t.charAt(g)!=e.charAt(y))throw"Unexpected literal at pos
ition "+g;g++},g=0;for(var y=0;y<e.length;y++)if(h)e.charAt(y)=="'"&&!p("'")?h=!1:m();else switch(e.charAt(y)){case"d":l=d("d");break;case"D":v("D",i,s);break;case"o":c=d("o");break;case"m":f=d("m");break;case"M":f=v("M",o,u);break;case"y":a=d("y");break;case"@":var b=new Date(d("@"));a=b.getFullYear(),f=b.getMonth()+1,l=b.getDate();break;case"!":var b=new Date((d("!")-this._ticksTo1970)/1e4);a=b.getFullYear(),f=b.getMonth()+1,l=b.getDate();break;case"'":p("'")?m():h=!0;break;default:m()}if(g<t.length){var w=t.substr(g);if(!/^\s+/.test(w))throw"Extra/unparsed characters found in date: "+w}a==-1?a=(new Date).getFullYear():a<100&&(a+=(new Date).getFullYear()-(new Date).getFullYear()%100+(a<=r?0:-100));if(c>-1){f=1,l=c;do{var E=this._getDaysInMonth(a,f-1);if(l<=E)break;f++,l-=E}while(!0)}var b=this._daylightSavingAdjust(new Date(a,f-1,l));if(b.getFullYear()!=a||b.getMonth()+1!=f||b.getDate()!=l)throw"Invalid date";return b},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd
",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925))*24*60*60*1e7,formatDate:function(e,t,n){if(!t)return"";var r=(n?n.dayNamesShort:null)||this._defaults.dayNamesShort,i=(n?n.dayNames:null)||this._defaults.dayNames,s=(n?n.monthNamesShort:null)||this._defaults.monthNamesShort,o=(n?n.monthNames:null)||this._defaults.monthNames,u=function(t){var n=h+1<e.length&&e.charAt(h+1)==t;return n&&h++,n},a=function(e,t,n){var r=""+t;if(u(e))while(r.length<n)r="0"+r;return r},f=function(e,t,n,r){return u(e)?r[t]:n[t]},l="",c=!1;if(t)for(var h=0;h<e.length;h++)if(c)e.charAt(h)=="'"&&!u("'")?c=!1:l+=e.charAt(h);else switch(e.charAt(h)){case"d":l+=a("d",t.getDate(),2);break;case"D":l+=f("D",t.getDay(),r,i);break;case"o":l+=a("o",Math.round(((new Date(t.getFullYear(),t.getMonth(),t.getDate())).getTime()-(new Date(t
.getFullYear(),0,0)).getTime())/864e5),3);break;case"m":l+=a("m",t.getMonth()+1,2);break;case"M":l+=f("M",t.getMonth(),s,o);break;case"y":l+=u("y")?t.getFullYear():(t.getYear()%100<10?"0":"")+t.getYear()%100;break;case"@":l+=t.getTime();break;case"!":l+=t.getTime()*1e4+this._ticksTo1970;break;case"'":u("'")?l+="'":c=!0;break;default:l+=e.charAt(h)}return l},_possibleChars:function(e){var t="",n=!1,r=function(t){var n=i+1<e.length&&e.charAt(i+1)==t;return n&&i++,n};for(var i=0;i<e.length;i++)if(n)e.charAt(i)=="'"&&!r("'")?n=!1:t+=e.charAt(i);else switch(e.charAt(i)){case"d":case"m":case"y":case"@":t+="0123456789";break;case"D":case"M":return null;case"'":r("'")?t+="'":n=!0;break;default:t+=e.charAt(i)}return t},_get:function(e,t){return e.settings[t]!==undefined?e.settings[t]:this._defaults[t]},_setDateFromField:function(e,t){if(e.input.val()==e.lastVal)return;var n=this._get(e,"dateFormat"),r=e.lastVal=e.input?e.input.val():null,i,s;i=s=this._getDefaultDate(e);var o=this._ge
tFormatConfig(e);try{i=this.parseDate(n,r,o)||s}catch(u){this.log(u),r=t?"":r}e.selectedDay=i.getDate(),e.drawMonth=e.selectedMonth=i.getMonth(),e.drawYear=e.selectedYear=i.getFullYear(),e.currentDay=r?i.getDate():0,e.currentMonth=r?i.getMonth():0,e.currentYear=r?i.getFullYear():0,this._adjustInstDate(e)},_getDefaultDate:function(e){return this._restrictMinMax(e,this._determineDate(e,this._get(e,"defaultDate"),new Date))},_determineDate:function(e,t,n){var r=function(e){var t=new Date;return t.setDate(t.getDate()+e),t},i=function(t){try{return $.datepicker.parseDate($.datepicker._get(e,"dateFormat"),t,$.datepicker._getFormatConfig(e))}catch(n){}var r=(t.toLowerCase().match(/^c/)?$.datepicker._getDate(e):null)||new Date,i=r.getFullYear(),s=r.getMonth(),o=r.getDate(),u=/([+-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,a=u.exec(t);while(a){switch(a[2]||"d"){case"d":case"D":o+=parseInt(a[1],10);break;case"w":case"W":o+=parseInt(a[1],10)*7;break;case"m":case"M":s+=parseInt(a[1],10),o=Math.mi
n(o,$.datepicker._getDaysInMonth(i,s));break;case"y":case"Y":i+=parseInt(a[1],10),o=Math.min(o,$.datepicker._getDaysInMonth(i,s))}a=u.exec(t)}return new Date(i,s,o)},s=t==null||t===""?n:typeof t=="string"?i(t):typeof t=="number"?isNaN(t)?n:r(t):new Date(t.getTime());return s=s&&s.toString()=="Invalid Date"?n:s,s&&(s.setHours(0),s.setMinutes(0),s.setSeconds(0),s.setMilliseconds(0)),this._daylightSavingAdjust(s)},_daylightSavingAdjust:function(e){return e?(e.setHours(e.getHours()>12?e.getHours()+2:0),e):null},_setDate:function(e,t,n){var r=!t,i=e.selectedMonth,s=e.selectedYear,o=this._restrictMinMax(e,this._determineDate(e,t,new Date));e.selectedDay=e.currentDay=o.getDate(),e.drawMonth=e.selectedMonth=e.currentMonth=o.getMonth(),e.drawYear=e.selectedYear=e.currentYear=o.getFullYear(),(i!=e.selectedMonth||s!=e.selectedYear)&&!n&&this._notifyChange(e),this._adjustInstDate(e),e.input&&e.input.val(r?"":this._formatDate(e))},_getDate:function(e){var t=!e.currentYear||e.input&&e.inp
ut.val()==""?null:this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return t},_attachHandlers:function(e){var t=this._get(e,"stepMonths"),n="#"+e.id.replace(/\\\\/g,"\\");e.dpDiv.find("[data-handler]").map(function(){var e={prev:function(){window["DP_jQuery_"+dpuuid].datepicker._adjustDate(n,-t,"M")},next:function(){window["DP_jQuery_"+dpuuid].datepicker._adjustDate(n,+t,"M")},hide:function(){window["DP_jQuery_"+dpuuid].datepicker._hideDatepicker()},today:function(){window["DP_jQuery_"+dpuuid].datepicker._gotoToday(n)},selectDay:function(){return window["DP_jQuery_"+dpuuid].datepicker._selectDay(n,+this.getAttribute("data-month"),+this.getAttribute("data-year"),this),!1},selectMonth:function(){return window["DP_jQuery_"+dpuuid].datepicker._selectMonthYear(n,this,"M"),!1},selectYear:function(){return window["DP_jQuery_"+dpuuid].datepicker._selectMonthYear(n,this,"Y"),!1}};$(this).bind(this.getAttribute("data-event"),e[this.getAttribute("data-hand
ler")])})},_generateHTML:function(e){var t=new Date;t=this._daylightSavingAdjust(new Date(t.getFullYear(),t.getMonth(),t.getDate()));var n=this._get(e,"isRTL"),r=this._get(e,"showButtonPanel"),i=this._get(e,"hideIfNoPrevNext"),s=this._get(e,"navigationAsDateFormat"),o=this._getNumberOfMonths(e),u=this._get(e,"showCurrentAtPos"),a=this._get(e,"stepMonths"),f=o[0]!=1||o[1]!=1,l=this._daylightSavingAdjust(e.currentDay?new Date(e.currentYear,e.currentMonth,e.currentDay):new Date(9999,9,9)),c=this._getMinMaxDate(e,"min"),h=this._getMinMaxDate(e,"max"),p=e.drawMonth-u,d=e.drawYear;p<0&&(p+=12,d--);if(h){var v=this._daylightSavingAdjust(new Date(h.getFullYear(),h.getMonth()-o[0]*o[1]+1,h.getDate()));v=c&&v<c?c:v;while(this._daylightSavingAdjust(new Date(d,p,1))>v)p--,p<0&&(p=11,d--)}e.drawMonth=p,e.drawYear=d;var m=this._get(e,"prevText");m=s?this.formatDate(m,this._daylightSavingAdjust(new Date(d,p-a,1)),this._getFormatConfig(e)):m;var g=this._canAdjustMonth(e,-1,d,p)?'<a class="u
i-datepicker-prev ui-corner-all" data-handler="prev" data-event="click" title="'+m+'"><span class="ui-icon ui-icon-circle-triangle-'+(n?"e":"w")+'">'+m+"</span></a>":i?"":'<a class="ui-datepicker-prev ui-corner-all ui-state-disabled" title="'+m+'"><span class="ui-icon ui-icon-circle-triangle-'+(n?"e":"w")+'">'+m+"</span></a>",y=this._get(e,"nextText");y=s?this.formatDate(y,this._daylightSavingAdjust(new Date(d,p+a,1)),this._getFormatConfig(e)):y;var b=this._canAdjustMonth(e,1,d,p)?'<a class="ui-datepicker-next ui-corner-all" data-handler="next" data-event="click" title="'+y+'"><span class="ui-icon ui-icon-circle-triangle-'+(n?"w":"e")+'">'+y+"</span></a>":i?"":'<a class="ui-datepicker-next ui-corner-all ui-state-disabled" title="'+y+'"><span class="ui-icon ui-icon-circle-triangle-'+(n?"w":"e")+'">'+y+"</span></a>",w=this._get(e,"currentText"),E=this._get(e,"gotoCurrent")&&e.currentDay?l:t;w=s?this.formatDate(w,E,this._getFormatConfig(e)):w;var S=e.inline?"":'<button type="bu
tton" class="ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all" data-handler="hide" data-event="click">'+this._get(e,"closeText")+"</button>",x=r?'<div class="ui-datepicker-buttonpane ui-widget-content">'+(n?S:"")+(this._isInRange(e,E)?'<button type="button" class="ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all" data-handler="today" data-event="click">'+w+"</button>":"")+(n?"":S)+"</div>":"",T=parseInt(this._get(e,"firstDay"),10);T=isNaN(T)?0:T;var N=this._get(e,"showWeek"),C=this._get(e,"dayNames"),k=this._get(e,"dayNamesShort"),L=this._get(e,"dayNamesMin"),A=this._get(e,"monthNames"),O=this._get(e,"monthNamesShort"),M=this._get(e,"beforeShowDay"),_=this._get(e,"showOtherMonths"),D=this._get(e,"selectOtherMonths"),P=this._get(e,"calculateWeek")||this.iso8601Week,H=this._getDefaultDate(e),B="";for(var j=0;j<o[0];j++){var F="";this.maxRows=4;for(var I=0;I<o[1];I++){var q=this._daylightSavingAdjust(new Date(d,p,e.selectedDay)),R
=" ui-corner-all",U="";if(f){U+='<div class="ui-datepicker-group';if(o[1]>1)switch(I){case 0:U+=" ui-datepicker-group-first",R=" ui-corner-"+(n?"right":"left");break;case o[1]-1:U+=" ui-datepicker-group-last",R=" ui-corner-"+(n?"left":"right");break;default:U+=" ui-datepicker-group-middle",R=""}U+='">'}U+='<div class="ui-datepicker-header ui-widget-header ui-helper-clearfix'+R+'">'+(/all|left/.test(R)&&j==0?n?b:g:"")+(/all|right/.test(R)&&j==0?n?g:b:"")+this._generateMonthYearHeader(e,p,d,c,h,j>0||I>0,A,O)+'</div><table class="ui-datepicker-calendar"><thead>'+"<tr>";var z=N?'<th class="ui-datepicker-week-col">'+this._get(e,"weekHeader")+"</th>":"";for(var W=0;W<7;W++){var X=(W+T)%7;z+="<th"+((W+T+6)%7>=5?' class="ui-datepicker-week-end"':"")+">"+'<span title="'+C[X]+'">'+L[X]+"</span></th>"}U+=z+"</tr></thead><tbody>";var V=this._getDaysInMonth(d,p);d==e.selectedYear&&p==e.selectedMonth&&(e.selectedDay=Math.min(e.selectedDay,V));var J=(this._getFirstDayOfMonth(d,p)-T+7)%7,K=
Math.ceil((J+V)/7),Q=f?this.maxRows>K?this.maxRows:K:K;this.maxRows=Q;var G=this._daylightSavingAdjust(new Date(d,p,1-J));for(var Y=0;Y<Q;Y++){U+="<tr>";var Z=N?'<td class="ui-datepicker-week-col">'+this._get(e,"calculateWeek")(G)+"</td>":"";for(var W=0;W<7;W++){var et=M?M.apply(e.input?e.input[0]:null,[G]):[!0,""],tt=G.getMonth()!=p,nt=tt&&!D||!et[0]||c&&G<c||h&&G>h;Z+='<td class="'+((W+T+6)%7>=5?" ui-datepicker-week-end":"")+(tt?" ui-datepicker-other-month":"")+(G.getTime()==q.getTime()&&p==e.selectedMonth&&e._keyEvent||H.getTime()==G.getTime()&&H.getTime()==q.getTime()?" "+this._dayOverClass:"")+(nt?" "+this._unselectableClass+" ui-state-disabled":"")+(tt&&!_?"":" "+et[1]+(G.getTime()==l.getTime()?" "+this._currentClass:"")+(G.getTime()==t.getTime()?" ui-datepicker-today":""))+'"'+((!tt||_)&&et[2]?' title="'+et[2]+'"':"")+(nt?"":' data-handler="selectDay" data-event="click" data-month="'+G.getMonth()+'" data-year="'+G.getFullYear()+'"')+">"+(tt&&!_?" ":nt?'<span clas
s="ui-state-default">'+G.getDate()+"</span>":'<a class="ui-state-default'+(G.getTime()==t.getTime()?" ui-state-highlight":"")+(G.getTime()==l.getTime()?" ui-state-active":"")+(tt?" ui-priority-secondary":"")+'" href="#">'+G.getDate()+"</a>")+"</td>",G.setDate(G.getDate()+1),G=this._daylightSavingAdjust(G)}U+=Z+"</tr>"}p++,p>11&&(p=0,d++),U+="</tbody></table>"+(f?"</div>"+(o[0]>0&&I==o[1]-1?'<div class="ui-datepicker-row-break"></div>':""):""),F+=U}B+=F}return B+=x+($.ui.ie6&&!e.inline?'<iframe src="javascript:false;" class="ui-datepicker-cover" frameborder="0"></iframe>':""),e._keyEvent=!1,B},_generateMonthYearHeader:function(e,t,n,r,i,s,o,u){var a=this._get(e,"changeMonth"),f=this._get(e,"changeYear"),l=this._get(e,"showMonthAfterYear"),c='<div class="ui-datepicker-title">',h="";if(s||!a)h+='<span class="ui-datepicker-month">'+o[t]+"</span>";else{var p=r&&r.getFullYear()==n,d=i&&i.getFullYear()==n;h+='<select class="ui-datepicker-month" data-handler="selectMonth" data-event
="change">';for(var v=0;v<12;v++)(!p||v>=r.getMonth())&&(!d||v<=i.getMonth())&&(h+='<option value="'+v+'"'+(v==t?' selected="selected"':"")+">"+u[v]+"</option>");h+="</select>"}l||(c+=h+(s||!a||!f?" ":""));if(!e.yearshtml){e.yearshtml="";if(s||!f)c+='<span class="ui-datepicker-year">'+n+"</span>";else{var m=this._get(e,"yearRange").split(":"),g=(new Date).getFullYear(),y=function(e){var t=e.match(/c[+-].*/)?n+parseInt(e.substring(1),10):e.match(/[+-].*/)?g+parseInt(e,10):parseInt(e,10);return isNaN(t)?g:t},b=y(m[0]),w=Math.max(b,y(m[1]||""));b=r?Math.max(b,r.getFullYear()):b,w=i?Math.min(w,i.getFullYear()):w,e.yearshtml+='<select class="ui-datepicker-year" data-handler="selectYear" data-event="change">';for(;b<=w;b++)e.yearshtml+='<option value="'+b+'"'+(b==n?' selected="selected"':"")+">"+b+"</option>";e.yearshtml+="</select>",c+=e.yearshtml,e.yearshtml=null}}return c+=this._get(e,"yearSuffix"),l&&(c+=(s||!a||!f?" ":"")+h),c+="</div>",c},_adjustInstDate:function(e
,t,n){var r=e.drawYear+(n=="Y"?t:0),i=e.drawMonth+(n=="M"?t:0),s=Math.min(e.selectedDay,this._getDaysInMonth(r,i))+(n=="D"?t:0),o=this._restrictMinMax(e,this._daylightSavingAdjust(new Date(r,i,s)));e.selectedDay=o.getDate(),e.drawMonth=e.selectedMonth=o.getMonth(),e.drawYear=e.selectedYear=o.getFullYear(),(n=="M"||n=="Y")&&this._notifyChange(e)},_restrictMinMax:function(e,t){var n=this._getMinMaxDate(e,"min"),r=this._getMinMaxDate(e,"max"),i=n&&t<n?n:t;return i=r&&i>r?r:i,i},_notifyChange:function(e){var t=this._get(e,"onChangeMonthYear");t&&t.apply(e.input?e.input[0]:null,[e.selectedYear,e.selectedMonth+1,e])},_getNumberOfMonths:function(e){var t=this._get(e,"numberOfMonths");return t==null?[1,1]:typeof t=="number"?[1,t]:t},_getMinMaxDate:function(e,t){return this._determineDate(e,this._get(e,t+"Date"),null)},_getDaysInMonth:function(e,t){return 32-this._daylightSavingAdjust(new Date(e,t,32)).getDate()},_getFirstDayOfMonth:function(e,t){return(new Date(e,t,1)).getDay()},_ca
nAdjustMonth:function(e,t,n,r){var i=this._getNumberOfMonths(e),s=this._daylightSavingAdjust(new Date(n,r+(t<0?t:i[0]*i[1]),1));return t<0&&s.setDate(this._getDaysInMonth(s.getFullYear(),s.getMonth())),this._isInRange(e,s)},_isInRange:function(e,t){var n=this._getMinMaxDate(e,"min"),r=this._getMinMaxDate(e,"max");return(!n||t.getTime()>=n.getTime())&&(!r||t.getTime()<=r.getTime())},_getFormatConfig:function(e){var t=this._get(e,"shortYearCutoff");return t=typeof t!="string"?t:(new Date).getFullYear()%100+parseInt(t,10),{shortYearCutoff:t,dayNamesShort:this._get(e,"dayNamesShort"),dayNames:this._get(e,"dayNames"),monthNamesShort:this._get(e,"monthNamesShort"),monthNames:this._get(e,"monthNames")}},_formatDate:function(e,t,n,r){t||(e.currentDay=e.selectedDay,e.currentMonth=e.selectedMonth,e.currentYear=e.selectedYear);var i=t?typeof t=="object"?t:this._daylightSavingAdjust(new Date(r,n,t)):this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return t
his.formatDate(this._get(e,"dateFormat"),i,this._getFormatConfig(e))}}),$.fn.datepicker=function(e){if(!this.length)return this;$.datepicker.initialized||($(document).mousedown($.datepicker._checkExternalClick).find(document.body).append($.datepicker.dpDiv),$.datepicker.initialized=!0);var t=Array.prototype.slice.call(arguments,1);return typeof e!="string"||e!="isDisabled"&&e!="getDate"&&e!="widget"?e=="option"&&arguments.length==2&&typeof arguments[1]=="string"?$.datepicker["_"+e+"Datepicker"].apply($.datepicker,[this[0]].concat(t)):this.each(function(){typeof e=="string"?$.datepicker["_"+e+"Datepicker"].apply($.datepicker,[this].concat(t)):$.datepicker._attachDatepicker(this,e)}):$.datepicker["_"+e+"Datepicker"].apply($.datepicker,[this[0]].concat(t))},$.datepicker=new Datepicker,$.datepicker.initialized=!1,$.datepicker.uuid=(new Date).getTime(),$.datepicker.version="1.9.2",window["DP_jQuery_"+dpuuid]=$})(jQuery);(function(e,t){var n="ui-dialog ui-widget ui-widget-content
ui-corner-all ",r={buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},i={maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0};e.widget("ui.dialog",{version:"1.9.2",options:{autoOpen:!0,buttons:{},closeOnEscape:!0,closeText:"close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:!1,maxWidth:!1,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",of:window,collision:"fit",using:function(t){var n=e(this).css(t).offset().top;n<0&&e(this).css("top",t.top-n)}},resizable:!0,show:null,stack:!0,title:"",width:300,zIndex:1e3},_create:function(){this.originalTitle=this.element.attr("title"),typeof this.originalTitle!="string"&&(this.originalTitle=""),this.oldPosition={parent:this.element.parent(),index:this.element.parent().children().index(this.element)},this.options.title=this.options.title||this.originalTitle;var t=this,r=this.options,i=r.title||" ",s,o,u,a,f;s=(this.uiDialog=e("<div>")).addClass(n+r.dialogClass).css({d
isplay:"none",outline:0,zIndex:r.zIndex}).attr("tabIndex",-1).keydown(function(n){r.closeOnEscape&&!n.isDefaultPrevented()&&n.keyCode&&n.keyCode===e.ui.keyCode.ESCAPE&&(t.close(n),n.preventDefault())}).mousedown(function(e){t.moveToTop(!1,e)}).appendTo("body"),this.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(s),o=(this.uiDialogTitlebar=e("<div>")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").bind("mousedown",function(){s.focus()}).prependTo(s),u=e("<a href='#'></a>").addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").click(function(e){e.preventDefault(),t.close(e)}).appendTo(o),(this.uiDialogTitlebarCloseText=e("<span>")).addClass("ui-icon ui-icon-closethick").text(r.closeText).appendTo(u),a=e("<span>").uniqueId().addClass("ui-dialog-title").html(i).prependTo(o),f=(this.uiDialogButtonPane=e("<div>")).addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix
"),(this.uiButtonSet=e("<div>")).addClass("ui-dialog-buttonset").appendTo(f),s.attr({role:"dialog","aria-labelledby":a.attr("id")}),o.find("*").add(o).disableSelection(),this._hoverable(u),this._focusable(u),r.draggable&&e.fn.draggable&&this._makeDraggable(),r.resizable&&e.fn.resizable&&this._makeResizable(),this._createButtons(r.buttons),this._isOpen=!1,e.fn.bgiframe&&s.bgiframe(),this._on(s,{keydown:function(t){if(!r.modal||t.keyCode!==e.ui.keyCode.TAB)return;var n=e(":tabbable",s),i=n.filter(":first"),o=n.filter(":last");if(t.target===o[0]&&!t.shiftKey)return i.focus(1),!1;if(t.target===i[0]&&t.shiftKey)return o.focus(1),!1}})},_init:function(){this.options.autoOpen&&this.open()},_destroy:function(){var e,t=this.oldPosition;this.overlay&&this.overlay.destroy(),this.uiDialog.hide(),this.element.removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body"),this.uiDialog.remove(),this.originalTitle&&this.element.attr("title",this.originalTitle),e=t.parent.childr
en().eq(t.index),e.length&&e[0]!==this.element[0]?e.before(this.element):t.parent.append(this.element)},widget:function(){return this.uiDialog},close:function(t){var n=this,r,i;if(!this._isOpen)return;if(!1===this._trigger("beforeClose",t))return;return this._isOpen=!1,this.overlay&&this.overlay.destroy(),this.options.hide?this._hide(this.uiDialog,this.options.hide,function(){n._trigger("close",t)}):(this.uiDialog.hide(),this._trigger("close",t)),e.ui.dialog.overlay.resize(),this.options.modal&&(r=0,e(".ui-dialog").each(function(){this!==n.uiDialog[0]&&(i=e(this).css("z-index"),isNaN(i)||(r=Math.max(r,i)))}),e.ui.dialog.maxZ=r),this},isOpen:function(){return this._isOpen},moveToTop:function(t,n){var r=this.options,i;return r.modal&&!t||!r.stack&&!r.modal?this._trigger("focus",n):(r.zIndex>e.ui.dialog.maxZ&&(e.ui.dialog.maxZ=r.zIndex),this.overlay&&(e.ui.dialog.maxZ+=1,e.ui.dialog.overlay.maxZ=e.ui.dialog.maxZ,this.overlay.$el.css("z-index",e.ui.dialog.overlay.maxZ)),i={scrol
lTop:this.element.scrollTop(),scrollLeft:this.element.scrollLeft()},e.ui.dialog.maxZ+=1,this.uiDialog.css("z-index",e.ui.dialog.maxZ),this.element.attr(i),this._trigger("focus",n),this)},open:function(){if(this._isOpen)return;var t,n=this.options,r=this.uiDialog;return this._size(),this._position(n.position),r.show(n.show),this.overlay=n.modal?new e.ui.dialog.overlay(this):null,this.moveToTop(!0),t=this.element.find(":tabbable"),t.length||(t=this.uiDialogButtonPane.find(":tabbable"),t.length||(t=r)),t.eq(0).focus(),this._isOpen=!0,this._trigger("open"),this},_createButtons:function(t){var n=this,r=!1;this.uiDialogButtonPane.remove(),this.uiButtonSet.empty(),typeof t=="object"&&t!==null&&e.each(t,function(){return!(r=!0)}),r?(e.each(t,function(t,r){var i,s;r=e.isFunction(r)?{click:r,text:t}:r,r=e.extend({type:"button"},r),s=r.click,r.click=function(){s.apply(n.element[0],arguments)},i=e("<button></button>",r).appendTo(n.uiButtonSet),e.fn.button&&i.button()}),this.uiDialog.add
Class("ui-dialog-buttons"),this.uiDialogButtonPane.appendTo(this.uiDialog)):this.uiDialog.removeClass("ui-dialog-buttons")},_makeDraggable:function(){function r(e){return{position:e.position,offset:e.offset}}var t=this,n=this.options;this.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(n,i){e(this).addClass("ui-dialog-dragging"),t._trigger("dragStart",n,r(i))},drag:function(e,n){t._trigger("drag",e,r(n))},stop:function(i,s){n.position=[s.position.left-t.document.scrollLeft(),s.position.top-t.document.scrollTop()],e(this).removeClass("ui-dialog-dragging"),t._trigger("dragStop",i,r(s)),e.ui.dialog.overlay.resize()}})},_makeResizable:function(n){function u(e){return{originalPosition:e.originalPosition,originalSize:e.originalSize,position:e.position,size:e.size}}n=n===t?this.options.resizable:n;var r=this,i=this.options,s=this.uiDialog.css("position"),o=typeof n=="string"?n:"n,e,s,w,se,
sw,ne,nw";this.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:this.element,maxWidth:i.maxWidth,maxHeight:i.maxHeight,minWidth:i.minWidth,minHeight:this._minHeight(),handles:o,start:function(t,n){e(this).addClass("ui-dialog-resizing"),r._trigger("resizeStart",t,u(n))},resize:function(e,t){r._trigger("resize",e,u(t))},stop:function(t,n){e(this).removeClass("ui-dialog-resizing"),i.height=e(this).height(),i.width=e(this).width(),r._trigger("resizeStop",t,u(n)),e.ui.dialog.overlay.resize()}}).css("position",s).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var e=this.options;return e.height==="auto"?e.minHeight:Math.min(e.minHeight,e.height)},_position:function(t){var n=[],r=[0,0],i;if(t){if(typeof t=="string"||typeof t=="object"&&"0"in t)n=t.split?t.split(" "):[t[0],t[1]],n.length===1&&(n[1]=n[0]),e.each(["left","top"],function(e,t){+n[e]===n[e]&&(r[e]=n[e],n[e]=t)}),t={my:n[0]+(r[0]<0?r[0]:"+"+r[0]
)+" "+n[1]+(r[1]<0?r[1]:"+"+r[1]),at:n.join(" ")};t=e.extend({},e.ui.dialog.prototype.options.position,t)}else t=e.ui.dialog.prototype.options.position;i=this.uiDialog.is(":visible"),i||this.uiDialog.show(),this.uiDialog.position(t),i||this.uiDialog.hide()},_setOptions:function(t){var n=this,s={},o=!1;e.each(t,function(e,t){n._setOption(e,t),e in r&&(o=!0),e in i&&(s[e]=t)}),o&&this._size(),this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option",s)},_setOption:function(t,r){var i,s,o=this.uiDialog;switch(t){case"buttons":this._createButtons(r);break;case"closeText":this.uiDialogTitlebarCloseText.text(""+r);break;case"dialogClass":o.removeClass(this.options.dialogClass).addClass(n+r);break;case"disabled":r?o.addClass("ui-dialog-disabled"):o.removeClass("ui-dialog-disabled");break;case"draggable":i=o.is(":data(draggable)"),i&&!r&&o.draggable("destroy"),!i&&r&&this._makeDraggable();break;case"position":this._position(r);break;case"resizable":s=o.is(":data(resizab
le)"),s&&!r&&o.resizable("destroy"),s&&typeof r=="string"&&o.resizable("option","handles",r),!s&&r!==!1&&this._makeResizable(r);break;case"title":e(".ui-dialog-title",this.uiDialogTitlebar).html(""+(r||" "))}this._super(t,r)},_size:function(){var t,n,r,i=this.options,s=this.uiDialog.is(":visible");this.element.show().css({width:"auto",minHeight:0,height:0}),i.minWidth>i.width&&(i.width=i.minWidth),t=this.uiDialog.css({height:"auto",width:i.width}).outerHeight(),n=Math.max(0,i.minHeight-t),i.height==="auto"?e.support.minHeight?this.element.css({minHeight:n,height:"auto"}):(this.uiDialog.show(),r=this.element.css("height","auto").height(),s||this.uiDialog.hide(),this.element.height(Math.max(r,n))):this.element.height(Math.max(i.height-t,0)),this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}}),e.extend(e.ui.dialog,{uuid:0,maxZ:0,getTitleId:function(e){var t=e.attr("id");return t||(this.uuid+=1,t=this.uuid),"ui-dialog-titl
e-"+t},overlay:function(t){this.$el=e.ui.dialog.overlay.create(t)}}),e.extend(e.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:e.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),function(e){return e+".dialog-overlay"}).join(" "),create:function(t){this.instances.length===0&&(setTimeout(function(){e.ui.dialog.overlay.instances.length&&e(document).bind(e.ui.dialog.overlay.events,function(t){if(e(t.target).zIndex()<e.ui.dialog.overlay.maxZ)return!1})},1),e(window).bind("resize.dialog-overlay",e.ui.dialog.overlay.resize));var n=this.oldInstances.pop()||e("<div>").addClass("ui-widget-overlay");return e(document).bind("keydown.dialog-overlay",function(r){var i=e.ui.dialog.overlay.instances;i.length!==0&&i[i.length-1]===n&&t.options.closeOnEscape&&!r.isDefaultPrevented()&&r.keyCode&&r.keyCode===e.ui.keyCode.ESCAPE&&(t.close(r),r.preventDefault())}),n.appendTo(document.body).css({width:this.width(),height:this.height()}),e.fn.bgiframe&&n.bgiframe(),this
.instances.push(n),n},destroy:function(t){var n=e.inArray(t,this.instances),r=0;n!==-1&&this.oldInstances.push(this.instances.splice(n,1)[0]),this.instances.length===0&&e([document,window]).unbind(".dialog-overlay"),t.height(0).width(0).remove(),e.each(this.instances,function(){r=Math.max(r,this.css("z-index"))}),this.maxZ=r},height:function(){var t,n;return e.ui.ie?(t=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight),n=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight),t<n?e(window).height()+"px":t+"px"):e(document).height()+"px"},width:function(){var t,n;return e.ui.ie?(t=Math.max(document.documentElement.scrollWidth,document.body.scrollWidth),n=Math.max(document.documentElement.offsetWidth,document.body.offsetWidth),t<n?e(window).width()+"px":t+"px"):e(document).width()+"px"},resize:function(){var t=e([]);e.each(e.ui.dialog.overlay.instances,function(){t=t.add(this)}),t.css({width:0,height:0}).css({width:e.ui.dialog.overlay
.width(),height:e.ui.dialog.overlay.height()})}}),e.extend(e.ui.dialog.overlay.prototype,{destroy:function(){e.ui.dialog.overlay.destroy(this.$el)}})})(jQuery);(function(e,t){var n=!1;e.widget("ui.menu",{version:"1.9.2",defaultElement:"<ul>",delay:300,options:{icons:{submenu:"ui-icon-carat-1-e"},menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.element.uniqueId().addClass("ui-menu ui-widget ui-widget-content ui-corner-all").toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length).attr({role:this.options.role,tabIndex:0}).bind("click"+this.eventNamespace,e.proxy(function(e){this.options.disabled&&e.preventDefault()},this)),this.options.disabled&&this.element.addClass("ui-state-disabled").attr("aria-disabled","true"),this._on({"mousedown .ui-menu-item > a":function(e){e.preventDefault()},"click .ui-state-disabled > a":function(e){e.preventDefault()},"click .ui-menu-it
em:has(a)":function(t){var r=e(t.target).closest(".ui-menu-item");!n&&r.not(".ui-state-disabled").length&&(n=!0,this.select(t),r.has(".ui-menu").length?this.expand(t):this.element.is(":focus")||(this.element.trigger("focus",[!0]),this.active&&this.active.parents(".ui-menu").length===1&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(t){var n=e(t.currentTarget);n.siblings().children(".ui-state-active").removeClass("ui-state-active"),this.focus(t,n)},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(e,t){var n=this.active||this.element.children(".ui-menu-item").eq(0);t||this.focus(e,n)},blur:function(t){this._delay(function(){e.contains(this.element[0],this.document[0].activeElement)||this.collapseAll(t)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){e(t.target).closest(".ui-menu").length||this.collapseAll(t),n=!1}})},_destroy:function(){this.element.removeAttr("aria-activedescendant").find(".ui-menu")
.andSelf().removeClass("ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons").removeAttr("role").removeAttr("tabIndex").removeAttr("aria-labelledby").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-disabled").removeUniqueId().show(),this.element.find(".ui-menu-item").removeClass("ui-menu-item").removeAttr("role").removeAttr("aria-disabled").children("a").removeUniqueId().removeClass("ui-corner-all ui-state-hover").removeAttr("tabIndex").removeAttr("role").removeAttr("aria-haspopup").children().each(function(){var t=e(this);t.data("ui-menu-submenu-carat")&&t.remove()}),this.element.find(".ui-menu-divider").removeClass("ui-menu-divider ui-widget-content")},_keydown:function(t){function a(e){return e.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}var n,r,i,s,o,u=!0;switch(t.keyCode){case e.ui.keyCode.PAGE_UP:this.previousPage(t);break;case e.ui.keyCode.PAGE_DOWN:this.nextPage(t);break;case e.ui.keyCode.HOME:this._move("first","first",t);break;c
ase e.ui.keyCode.END:this._move("last","last",t);break;case e.ui.keyCode.UP:this.previous(t);break;case e.ui.keyCode.DOWN:this.next(t);break;case e.ui.keyCode.LEFT:this.collapse(t);break;case e.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(t);break;case e.ui.keyCode.ENTER:case e.ui.keyCode.SPACE:this._activate(t);break;case e.ui.keyCode.ESCAPE:this.collapse(t);break;default:u=!1,r=this.previousFilter||"",i=String.fromCharCode(t.keyCode),s=!1,clearTimeout(this.filterTimer),i===r?s=!0:i=r+i,o=new RegExp("^"+a(i),"i"),n=this.activeMenu.children(".ui-menu-item").filter(function(){return o.test(e(this).children("a").text())}),n=s&&n.index(this.active.next())!==-1?this.active.nextAll(".ui-menu-item"):n,n.length||(i=String.fromCharCode(t.keyCode),o=new RegExp("^"+a(i),"i"),n=this.activeMenu.children(".ui-menu-item").filter(function(){return o.test(e(this).children("a").text())})),n.length?(this.focus(t,n),n.length>1?(this.previousFilter=i,this.fil
terTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter):delete this.previousFilter}u&&t.preventDefault()},_activate:function(e){this.active.is(".ui-state-disabled")||(this.active.children("a[aria-haspopup='true']").length?this.expand(e):this.select(e))},refresh:function(){var t,n=this.options.icons.submenu,r=this.element.find(this.options.menus);r.filter(":not(.ui-menu)").addClass("ui-menu ui-widget ui-widget-content ui-corner-all").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var t=e(this),r=t.prev("a"),i=e("<span>").addClass("ui-menu-icon ui-icon "+n).data("ui-menu-submenu-carat",!0);r.attr("aria-haspopup","true").prepend(i),t.attr("aria-labelledby",r.attr("id"))}),t=r.add(this.element),t.children(":not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","presentation").children("a").uniqueId().addClass("ui-corner-all").attr({tabIndex:-1,role:this._itemRole()}),t.children("
:not(.ui-menu-item)").each(function(){var t=e(this);/[^\-—–\s]/.test(t.text())||t.addClass("ui-widget-content ui-menu-divider")}),t.children(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!e.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},focus:function(e,t){var n,r;this.blur(e,e&&e.type==="focus"),this._scrollIntoView(t),this.active=t.first(),r=this.active.children("a").addClass("ui-state-focus"),this.options.role&&this.element.attr("aria-activedescendant",r.attr("id")),this.active.parent().closest(".ui-menu-item").children("a:first").addClass("ui-state-active"),e&&e.type==="keydown"?this._close():this.timer=this._delay(function(){this._close()},this.delay),n=t.children(".ui-menu"),n.length&&/^mouse/.test(e.type)&&this._startOpening(n),this.activeMenu=t.parent(),this._trigger("focus",e,{item:t})},_scrollIntoView:function(t){var n,r,i,s,o,u;this._hasScroll()&&(n=pars
eFloat(e.css(this.activeMenu[0],"borderTopWidth"))||0,r=parseFloat(e.css(this.activeMenu[0],"paddingTop"))||0,i=t.offset().top-this.activeMenu.offset().top-n-r,s=this.activeMenu.scrollTop(),o=this.activeMenu.height(),u=t.height(),i<0?this.activeMenu.scrollTop(s+i):i+u>o&&this.activeMenu.scrollTop(s+i-o+u))},blur:function(e,t){t||clearTimeout(this.timer);if(!this.active)return;this.active.children("a").removeClass("ui-state-focus"),this.active=null,this._trigger("blur",e,{item:this.active})},_startOpening:function(e){clearTimeout(this.timer);if(e.attr("aria-hidden")!=="true")return;this.timer=this._delay(function(){this._close(),this._open(e)},this.delay)},_open:function(t){var n=e.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(t.parents(".ui-menu")).hide().attr("aria-hidden","true"),t.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(n)},collapseAll:function(t,n){clearTimeout(this.timer),this.tim
er=this._delay(function(){var r=n?this.element:e(t&&t.target).closest(this.element.find(".ui-menu"));r.length||(r=this.element),this._close(r),this.blur(t),this.activeMenu=r},this.delay)},_close:function(e){e||(e=this.active?this.active.parent():this.element),e.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false").end().find("a.ui-state-active").removeClass("ui-state-active")},collapse:function(e){var t=this.active&&this.active.parent().closest(".ui-menu-item",this.element);t&&t.length&&(this._close(),this.focus(e,t))},expand:function(e){var t=this.active&&this.active.children(".ui-menu ").children(".ui-menu-item").first();t&&t.length&&(this._open(t.parent()),this._delay(function(){this.focus(e,t)}))},next:function(e){this._move("next","first",e)},previous:function(e){this._move("prev","last",e)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".
ui-menu-item").length},_move:function(e,t,n){var r;this.active&&(e==="first"||e==="last"?r=this.active[e==="first"?"prevAll":"nextAll"](".ui-menu-item").eq(-1):r=this.active[e+"All"](".ui-menu-item").eq(0));if(!r||!r.length||!this.active)r=this.activeMenu.children(".ui-menu-item")[t]();this.focus(n,r)},nextPage:function(t){var n,r,i;if(!this.active){this.next(t);return}if(this.isLastItem())return;this._hasScroll()?(r=this.active.offset().top,i=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return n=e(this),n.offset().top-r-i<0}),this.focus(t,n)):this.focus(t,this.activeMenu.children(".ui-menu-item")[this.active?"last":"first"]())},previousPage:function(t){var n,r,i;if(!this.active){this.next(t);return}if(this.isFirstItem())return;this._hasScroll()?(r=this.active.offset().top,i=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return n=e(this),n.offset().top-r+i>0}),this.focus(t,n)):this.focus(t,this.activeMenu.children(".u
i-menu-item").first())},_hasScroll:function(){return this.element.outerHeight()<this.element.prop("scrollHeight")},select:function(t){this.active=this.active||e(t.target).closest(".ui-menu-item");var n={item:this.active};this.active.has(".ui-menu").length||this.collapseAll(t,!0),this._trigger("select",t,n)}})})(jQuery);(function(e){function t(e){return function(){var t=this.element.val();e.apply(this,arguments),this._refresh(),t!==this.element.val()&&this._trigger("change")}}e.widget("ui.spinner",{version:"1.9.2",defaultElement:"<input>",widgetEventPrefix:"spin",options:{culture:null,icons:{down:"ui-icon-triangle-1-s",up:"ui-icon-triangle-1-n"},incremental:!0,max:null,min:null,numberFormat:null,page:10,step:1,change:null,spin:null,start:null,stop:null},_create:function(){this._setOption("max",this.options.max),this._setOption("min",this.options.min),this._setOption("step",this.options.step),this._value(this.element.val(),!0),this._draw(),this._on(this._events),this._refresh(
),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_getCreateOptions:function(){var t={},n=this.element;return e.each(["min","max","step"],function(e,r){var i=n.attr(r);i!==undefined&&i.length&&(t[r]=i)}),t},_events:{keydown:function(e){this._start(e)&&this._keydown(e)&&e.preventDefault()},keyup:"_stop",focus:function(){this.previous=this.element.val()},blur:function(e){if(this.cancelBlur){delete this.cancelBlur;return}this._refresh(),this.previous!==this.element.val()&&this._trigger("change",e)},mousewheel:function(e,t){if(!t)return;if(!this.spinning&&!this._start(e))return!1;this._spin((t>0?1:-1)*this.options.step,e),clearTimeout(this.mousewheelTimer),this.mousewheelTimer=this._delay(function(){this.spinning&&this._stop(e)},100),e.preventDefault()},"mousedown .ui-spinner-button":function(t){function r(){var e=this.element[0]===this.document[0].activeElement;e||(this.element.focus(),this.previous=n,this._delay(function(){this.previou
s=n}))}var n;n=this.element[0]===this.document[0].activeElement?this.previous:this.element.val(),t.preventDefault(),r.call(this),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur,r.call(this)});if(this._start(t)===!1)return;this._repeat(null,e(t.currentTarget).hasClass("ui-spinner-up")?1:-1,t)},"mouseup .ui-spinner-button":"_stop","mouseenter .ui-spinner-button":function(t){if(!e(t.currentTarget).hasClass("ui-state-active"))return;if(this._start(t)===!1)return!1;this._repeat(null,e(t.currentTarget).hasClass("ui-spinner-up")?1:-1,t)},"mouseleave .ui-spinner-button":"_stop"},_draw:function(){var e=this.uiSpinner=this.element.addClass("ui-spinner-input").attr("autocomplete","off").wrap(this._uiSpinnerHtml()).parent().append(this._buttonHtml());this.element.attr("role","spinbutton"),this.buttons=e.find(".ui-spinner-button").attr("tabIndex",-1).button().removeClass("ui-corner-all"),this.buttons.height()>Math.ceil(e.height()*.5)&&e.height()>0&&e.height(e.height()),t
his.options.disabled&&this.disable()},_keydown:function(t){var n=this.options,r=e.ui.keyCode;switch(t.keyCode){case r.UP:return this._repeat(null,1,t),!0;case r.DOWN:return this._repeat(null,-1,t),!0;case r.PAGE_UP:return this._repeat(null,n.page,t),!0;case r.PAGE_DOWN:return this._repeat(null,-n.page,t),!0}return!1},_uiSpinnerHtml:function(){return"<span class='ui-spinner ui-widget ui-widget-content ui-corner-all'></span>"},_buttonHtml:function(){return"<a class='ui-spinner-button ui-spinner-up ui-corner-tr'><span class='ui-icon "+this.options.icons.up+"'>▲</span>"+"</a>"+"<a class='ui-spinner-button ui-spinner-down ui-corner-br'>"+"<span class='ui-icon "+this.options.icons.down+"'>▼</span>"+"</a>"},_start:function(e){return!this.spinning&&this._trigger("start",e)===!1?!1:(this.counter||(this.counter=1),this.spinning=!0,!0)},_repeat:function(e,t,n){e=e||500,clearTimeout(this.timer),this.timer=this._delay(function(){this._repeat(40,t,n)},e),this._spin(t*this.opti
ons.step,n)},_spin:function(e,t){var n=this.value()||0;this.counter||(this.counter=1),n=this._adjustValue(n+e*this._increment(this.counter));if(!this.spinning||this._trigger("spin",t,{value:n})!==!1)this._value(n),this.counter++},_increment:function(t){var n=this.options.incremental;return n?e.isFunction(n)?n(t):Math.floor(t*t*t/5e4-t*t/500+17*t/200+1):1},_precision:function(){var e=this._precisionOf(this.options.step);return this.options.min!==null&&(e=Math.max(e,this._precisionOf(this.options.min))),e},_precisionOf:function(e){var t=e.toString(),n=t.indexOf(".");return n===-1?0:t.length-n-1},_adjustValue:function(e){var t,n,r=this.options;return t=r.min!==null?r.min:0,n=e-t,n=Math.round(n/r.step)*r.step,e=t+n,e=parseFloat(e.toFixed(this._precision())),r.max!==null&&e>r.max?r.max:r.min!==null&&e<r.min?r.min:e},_stop:function(e){if(!this.spinning)return;clearTimeout(this.timer),clearTimeout(this.mousewheelTimer),this.counter=0,this.spinning=!1,this._trigger("stop",e)},_setOp
tion:function(e,t){if(e==="culture"||e==="numberFormat"){var n=this._parse(this.element.val());this.options[e]=t,this.element.val(this._format(n));return}(e==="max"||e==="min"||e==="step")&&typeof t=="string"&&(t=this._parse(t)),this._super(e,t),e==="disabled"&&(t?(this.element.prop("disabled",!0),this.buttons.button("disable")):(this.element.prop("disabled",!1),this.buttons.button("enable")))},_setOptions:t(function(e){this._super(e),this._value(this.element.val())}),_parse:function(e){return typeof e=="string"&&e!==""&&(e=window.Globalize&&this.options.numberFormat?Globalize.parseFloat(e,10,this.options.culture):+e),e===""||isNaN(e)?null:e},_format:function(e){return e===""?"":window.Globalize&&this.options.numberFormat?Globalize.format(e,this.options.numberFormat,this.options.culture):e},_refresh:function(){this.element.attr({"aria-valuemin":this.options.min,"aria-valuemax":this.options.max,"aria-valuenow":this._parse(this.element.val())})},_value:function(e,t){var n;e!==
""&&(n=this._parse(e),n!==null&&(t||(n=this._adjustValue(n)),e=this._format(n))),this.element.val(e),this._refresh()},_destroy:function(){this.element.removeClass("ui-spinner-input").prop("disabled",!1).removeAttr("autocomplete").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.uiSpinner.replaceWith(this.element)},stepUp:t(function(e){this._stepUp(e)}),_stepUp:function(e){this._spin((e||1)*this.options.step)},stepDown:t(function(e){this._stepDown(e)}),_stepDown:function(e){this._spin((e||1)*-this.options.step)},pageUp:t(function(e){this._stepUp((e||1)*this.options.page)}),pageDown:t(function(e){this._stepDown((e||1)*this.options.page)}),value:function(e){if(!arguments.length)return this._parse(this.element.val());t(this._value).call(this,e)},widget:function(){return this.uiSpinner}})})(jQuery);
\ No newline at end of file
commit b921882b2beadf67b9d2898e063eb48bd0748843
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:55:06 2014 +0100
Add the images used by jQuery
diff --git a/fedora_elections/static/images/ui-bg_flat_0_aaaaaa_40x100.png b/fedora_elections/static/images/ui-bg_flat_0_aaaaaa_40x100.png
new file mode 100644
index 0000000..5b5dab2
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_flat_0_aaaaaa_40x100.png differ
diff --git a/fedora_elections/static/images/ui-bg_flat_0_eeeeee_40x100.png b/fedora_elections/static/images/ui-bg_flat_0_eeeeee_40x100.png
new file mode 100644
index 0000000..e44f861
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_flat_0_eeeeee_40x100.png differ
diff --git a/fedora_elections/static/images/ui-bg_flat_55_ffffff_40x100.png b/fedora_elections/static/images/ui-bg_flat_55_ffffff_40x100.png
new file mode 100644
index 0000000..ac8b229
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_flat_55_ffffff_40x100.png differ
diff --git a/fedora_elections/static/images/ui-bg_flat_75_ffffff_40x100.png b/fedora_elections/static/images/ui-bg_flat_75_ffffff_40x100.png
new file mode 100644
index 0000000..ac8b229
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_flat_75_ffffff_40x100.png differ
diff --git a/fedora_elections/static/images/ui-bg_glass_65_ffffff_1x400.png b/fedora_elections/static/images/ui-bg_glass_65_ffffff_1x400.png
new file mode 100644
index 0000000..42ccba2
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_glass_65_ffffff_1x400.png differ
diff --git a/fedora_elections/static/images/ui-bg_highlight-soft_100_f6f6f6_1x100.png b/fedora_elections/static/images/ui-bg_highlight-soft_100_f6f6f6_1x100.png
new file mode 100644
index 0000000..5dcfaa9
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_highlight-soft_100_f6f6f6_1x100.png differ
diff --git a/fedora_elections/static/images/ui-bg_highlight-soft_25_3c6eb4_1x100.png b/fedora_elections/static/images/ui-bg_highlight-soft_25_3c6eb4_1x100.png
new file mode 100644
index 0000000..a9ffc07
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_highlight-soft_25_3c6eb4_1x100.png differ
diff --git a/fedora_elections/static/images/ui-bg_highlight-soft_50_dddddd_1x100.png b/fedora_elections/static/images/ui-bg_highlight-soft_50_dddddd_1x100.png
new file mode 100644
index 0000000..b47a4da
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_highlight-soft_50_dddddd_1x100.png differ
diff --git a/fedora_elections/static/images/ui-bg_highlight-soft_75_dddddd_1x100.png b/fedora_elections/static/images/ui-bg_highlight-soft_75_dddddd_1x100.png
new file mode 100644
index 0000000..8392ac7
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_highlight-soft_75_dddddd_1x100.png differ
diff --git a/fedora_elections/static/images/ui-icons_3c6eb4_256x240.png b/fedora_elections/static/images/ui-icons_3c6eb4_256x240.png
new file mode 100644
index 0000000..ecf79bc
Binary files /dev/null and b/fedora_elections/static/images/ui-icons_3c6eb4_256x240.png differ
diff --git a/fedora_elections/static/images/ui-icons_454545_256x240.png b/fedora_elections/static/images/ui-icons_454545_256x240.png
new file mode 100644
index 0000000..59bd45b
Binary files /dev/null and b/fedora_elections/static/images/ui-icons_454545_256x240.png differ
diff --git a/fedora_elections/static/images/ui-icons_666666_256x240.png b/fedora_elections/static/images/ui-icons_666666_256x240.png
new file mode 100644
index 0000000..f87de1c
Binary files /dev/null and b/fedora_elections/static/images/ui-icons_666666_256x240.png differ
diff --git a/fedora_elections/static/images/ui-icons_db3279_256x240.png b/fedora_elections/static/images/ui-icons_db3279_256x240.png
new file mode 100644
index 0000000..67b5713
Binary files /dev/null and b/fedora_elections/static/images/ui-icons_db3279_256x240.png differ
diff --git a/fedora_elections/static/images/ui-icons_ffffff_256x240.png b/fedora_elections/static/images/ui-icons_ffffff_256x240.png
new file mode 100644
index 0000000..42f8f99
Binary files /dev/null and b/fedora_elections/static/images/ui-icons_ffffff_256x240.png differ
commit 8a7fe9955abc63cea5f7966e39386a76010fcde9
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:54:51 2014 +0100
Add the election logo
diff --git a/fedora_elections/static/elections-logo.png b/fedora_elections/static/elections-logo.png
new file mode 100644
index 0000000..9ef1ec3
Binary files /dev/null and b/fedora_elections/static/elections-logo.png differ
commit fbbc1340bab17f09a0fd47db6b727cae29b2e846
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:54:43 2014 +0100
If there is no elections, keep going this will be our admin index page
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 5747469..4907395 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -441,10 +441,6 @@ def admin_view_elections():
elections = models.Election.search(
SESSION, fas_user=flask.g.fas_user.username)
- if not elections:
- flask.flash('You do not have any elections.')
- return redirect.safe_redirect_back()
-
return flask.render_template(
'admin/all_elections.html',
elections=elections)
commit 6eee058b8f7b68fc709a4b30f64a9e04ed74f8b4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:29:24 2014 +0100
Inject some variables into every templates
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 1905d8b..5747469 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -131,6 +131,17 @@ def get_valid_election(election_alias, ended=False):
return election
+(a)APP.context_processor
+def inject_variables():
+ ''' Inject a set of variable that we want for every pages (every
+ template).
+ '''
+ user = None
+ if hasattr(flask.g, 'fas_user'):
+ user = flask.g.fas_user
+ return dict(is_admin=is_elections_admin(user),
+ version=__version__)
+
### LIST VIEWS #############################################
commit 9300114324a2d2900024141a35702e8c94d0f180
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:29:02 2014 +0100
Add a top level __version__ and use this in the setup.py
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 55d4725..1905d8b 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -24,6 +24,8 @@
# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
+__version__ = '0.2.0'
+
import flask
from flask.ext.fas_openid import FAS
diff --git a/setup.py b/setup.py
index e71e5b9..b05b39b 100644
--- a/setup.py
+++ b/setup.py
@@ -2,15 +2,17 @@
__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
import pkg_resources
+from fedora_elections import __version__
from setuptools import setup, find_packages
-setup( name = 'fedora-elections'
- , version = '0.2'
- , author = 'Frank Chiulli'
- , author_email = 'fchiulli(a)fedoraproject.org'
- , packages = find_packages()
- , zip_safe = False
- , include_package_data = True
- , install_requires = ['Flask', 'SQLAlchemy>=0.7', 'python-fedora']
- )
+setup(
+ name = 'fedora-elections',
+ version = __version__,
+ author = 'Frank Chiulli',
+ author_email = 'fchiulli(a)fedoraproject.org',
+ packages = find_packages(),
+ zip_safe = False,
+ include_package_data = True,
+ install_requires = ['Flask', 'SQLAlchemy>=0.7', 'python-fedora'],
+)
commit 54d9636866de98e8b95cc1fc3854e6b2afe4c488
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:26:35 2014 +0100
Create a dedicated function to check is the user is an admin
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 8ac3eb8..55d4725 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -63,9 +63,21 @@ from fedora_elections import forms
from fedora_elections import redirect
-def remove_csrf(form_data):
- return dict([(k, v) for k, v in form_data.items() if k != 'csrf'])
+def is_elections_admin(user):
+ ''' Is the user an elections admin.
+ '''
+ if not user:
+ return False
+ if not user.cla_done or len(user.groups) < 1:
+ return False
+
+ admins = APP.config['FEDORA_ELECTIONS_ADMIN_GROUP']
+ if isinstance(admins, basestring): # pragma: no cover
+ admins = set([admins])
+ else:
+ admins = set(admins)
+ return len(set(user.groups).intersection(admins)) > 0
def login_required(f):
@wraps(f)
@@ -83,8 +95,7 @@ def election_admin_required(f):
if not hasattr(flask.g, 'fas_user') or flask.g.fas_user is None:
return flask.redirect(flask.url_for(
'auth_login', next=flask.request.url))
- if APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
- flask.g.fas_user.groups:
+ if not is_elections_admin(flask.g.fas_user):
flask.abort(403)
return f(*args, **kwargs)
return decorated_function
commit bb4d8b11b59a505aa9c98e67458bba46f73efdbd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:20:39 2014 +0100
Fix link, we have auth_login not login here
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 8f61812..8ac3eb8 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -392,7 +392,7 @@ def auth_login():
if 'next' in flask.request.args:
next_url = flask.request.args['next']
- if not next_url or next_url == flask.url_for('.login'):
+ if not next_url or next_url == flask.url_for('.auth_login'):
next_url = flask.url_for('.index')
if hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None:
commit 205bab3fb34afe8722b02ccae54214371ca693f2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:16:53 2014 +0100
Add some missing dependencies
diff --git a/requirements.txt b/requirements.txt
index 3887d38..ca5b050 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,4 +4,6 @@ flask
kitchen
python-fedora
Flask-wtf
-
+python-openid
+python-openid-cla
+python-openid-teams
commit 5bc2cf82f27297ed34cbd892686ee8a9603188f8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:58:51 2014 +0100
Remove the login template, no longer used
diff --git a/fedora_elections/templates/auth/login.html b/fedora_elections/templates/auth/login.html
deleted file mode 100644
index 9e7b73c..0000000
--- a/fedora_elections/templates/auth/login.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}Log in{% endblock %}
-{% block body %}
- <h2>Log in</h2>
-
- <form action="" method="post">
- <p>
- Log in using your
- <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a>
- account.
- </p>
- {{ render_field(form.username) }}
- {{ render_field(form.password) }}
- <p class="buttons indent"><input type="submit" class="submit positive"
- value="Log in">
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
commit b2ec7df1a113ab1b64a54320847055779074344d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:58:15 2014 +0100
Little clean up in the master template
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
index 3e6f806..47ab8bc 100644
--- a/fedora_elections/templates/base.html
+++ b/fedora_elections/templates/base.html
@@ -48,24 +48,24 @@
{% if g.fas_user %}
<li>
Hello, {{ g.fas_user.username }} —
- <a href="{{ url_for("auth_logout") }}">log out</a>
+ <a href="{{ url_for('auth_logout') }}">log out</a>
</li>
{% else %}
<li>
You are not logged in —
- <a href="{{ url_for("auth_login") }}">log in</a>
+ <a href="{{ url_for('auth_login') }}">log in</a>
</li>
{% endif %}
</ul>
<h2>Elections</h2>
<ul>
<li>
- <a href="{{ url_for("open_elections") }}">
+ <a href="{{ url_for('open_elections') }}">
Open elections
</a>
</li>
<li>
- <a href="{{ url_for("archived_elections") }}">
+ <a href="{{ url_for('archived_elections') }}">
Archived elections
</a>
</li>
@@ -74,12 +74,12 @@
<h2>Administration</h2>
<ul>
<li>
- <a href="{{ url_for("admin_view_elections") }}">
+ <a href="{{ url_for('admin_view_elections') }}">
My elections
</a>
</li>
<li>
- <a href="{{ url_for("admin_new_election") }}">
+ <a href="{{ url_for('admin_new_election') }}">
Create new election
</a>
</li>
@@ -101,7 +101,7 @@
<div id="footer">
{% block footer %}
<p class="copy">
- Copyright © 2008 Red Hat, Inc. and others. Please send any
+ Copyright © 2014 Red Hat, Inc. and others. Please send any
comments or corrections to the
<a href="mailto:webmaster@fedoraproject.org">
websites team
commit b00ae49e4e8d8eec971822d6078fe34e61e77d0b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:58:05 2014 +0100
Remove the login form, no longer needed
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 63a38c6..26d602c 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -90,12 +90,5 @@ class CandidateForm(wtf.Form):
url = wtforms.TextField('URL', [wtforms.validators.Length(max=250)])
-class LoginForm(wtf.Form):
- username = wtforms.TextField(
- 'Username', [wtforms.validators.Required()])
- password = wtforms.PasswordField(
- 'Password', [wtforms.validators.Required()])
-
-
class ConfirmationForm(wtf.Form):
pass
commit f6b0d247ddb6071b87f6d627fd48e430e7fcfc17
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:57:55 2014 +0100
Adjust the requirements to list python-fedora and not flask-fas
diff --git a/requirements.txt b/requirements.txt
index 336fbab..3887d38 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,6 +3,5 @@
flask
kitchen
python-fedora
-Flask-FAS
Flask-wtf
diff --git a/setup.py b/setup.py
index b05d9ba..e71e5b9 100644
--- a/setup.py
+++ b/setup.py
@@ -12,5 +12,5 @@ setup( name = 'fedora-elections'
, packages = find_packages()
, zip_safe = False
, include_package_data = True
- , install_requires = ['Flask', 'SQLAlchemy>=0.7']
+ , install_requires = ['Flask', 'SQLAlchemy>=0.7', 'python-fedora']
)
commit 07e088331296bd7b53bbcd595beb1b69a36e12b4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:57:18 2014 +0100
Port from flask-fas to flask-fas-openid
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index a073eae..8f61812 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -25,7 +25,7 @@
#
import flask
-from flask.ext.fas import FAS
+from flask.ext.fas_openid import FAS
from fedora.client import AuthError, AppError
from fedora.client.fas2 import AccountSystem
@@ -70,9 +70,9 @@ def remove_csrf(form_data):
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
- if flask.g.fas_user is None:
- return flask.redirect(flask.url_for('auth_login',
- next=flask.request.url))
+ if not hasattr(flask.g, 'fas_user') or flask.g.fas_user is None:
+ return flask.redirect(flask.url_for(
+ 'auth_login', next=flask.request.url))
return f(*args, **kwargs)
return decorated_function
@@ -80,9 +80,9 @@ def login_required(f):
def election_admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
- if flask.g.fas_user is None:
- return flask.redirect(flask.url_for('auth_login',
- next=flask.request.url))
+ if not hasattr(flask.g, 'fas_user') or flask.g.fas_user is None:
+ return flask.redirect(flask.url_for(
+ 'auth_login', next=flask.request.url))
if APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
flask.g.fas_user.groups:
flask.abort(403)
@@ -388,24 +388,24 @@ def vote_simple(election_alias):
def auth_login():
if flask.g.fas_user:
return redirect.safe_redirect_back()
- form = forms.LoginForm()
- if form.validate_on_submit():
- if FAS.login(form.username.data, form.password.data):
- flask.flash('Welcome, %s' % flask.g.fas_user.username)
- return redirect.safe_redirect_back()
- else:
- flask.flash('Incorrect username or password')
- return flask.render_template(
- 'auth/login.html',
- form=form)
+ next_url = None
+ if 'next' in flask.request.args:
+ next_url = flask.request.args['next']
+
+ if not next_url or next_url == flask.url_for('.login'):
+ next_url = flask.url_for('.index')
+
+ if hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None:
+ return flask.redirect(next_url)
+ else:
+ return FAS.login(return_url=next_url)
@APP.route('/logout')
def auth_logout():
- if not flask.g.fas_user:
- return redirect.safe_redirect_back()
- fas.logout()
- flask.flash('You have been logged out')
+ if hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None:
+ FAS.logout()
+ flask.flash('You have been logged out')
return redirect.safe_redirect_back()
commit 1ae930d03cc433fc8de7ecc4030e6583093f3159
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sun Mar 9 20:53:59 2014 +0100
Style change
Let's be consistent throughout the application on how we write the render_template
arguments.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 97817ce..a073eae 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -374,10 +374,12 @@ def vote_simple(election_alias):
usernamemap[candidate.id] = candidate.name
return flask.render_template(
- 'election/vote_simple.html', election=election,
+ 'election/vote_simple.html',
+ election=election,
num_candidates=num_candidates,
usernamemap=usernamemap,
- candidate_id=candidate_id, nextaction=next_action)
+ candidate_id=candidate_id,
+ nextaction=next_action)
### AUTH VIEWS #############################################
@@ -393,7 +395,9 @@ def auth_login():
return redirect.safe_redirect_back()
else:
flask.flash('Incorrect username or password')
- return flask.render_template('auth/login.html', form=form)
+ return flask.render_template(
+ 'auth/login.html',
+ form=form)
@APP.route('/logout')
commit 0d9bd43f51c74646c3717e31b0769bb1ebba82ba
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat Mar 1 07:49:39 2014 +0100
pep8 fixes
diff --git a/fedmsg.d/base.py b/fedmsg.d/base.py
index dc1d155..581c207 100644
--- a/fedmsg.d/base.py
+++ b/fedmsg.d/base.py
@@ -32,8 +32,8 @@ config = dict(
# opposed to amqp or stomp.
zmq_enabled=True,
- # When subscribing to messages, we want to allow splats ('*') so we tell the
- # hub to not be strict when comparing messages topics to subscription
+ # When subscribing to messages, we want to allow splats ('*') so we tell
+ # the hub to not be strict when comparing messages topics to subscription
# topics.
zmq_strict=False,
)
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 5cf5e68..97817ce 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -238,10 +238,10 @@ def vote_range(election_alias):
uvotes = {}
for candidate in election.candidates:
uvotes[candidate.id] = ""
- next_action = 'vote'
+ next_action = 'vote'
if flask.request.method == 'POST':
- next_action = ''
+ next_action = ''
form_values = flask.request.form.values()
if 'Submit' in form_values:
for candidate in election.candidates:
@@ -310,6 +310,7 @@ def vote_range(election_alias):
voteinfo=uvotes,
nextaction=next_action)
+
@APP.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
@login_required
def vote_simple(election_alias):
@@ -330,11 +331,11 @@ def vote_simple(election_alias):
return redirect.safe_redirect_back()
num_candidates = election.candidates.count()
- next_action = 'vote'
+ next_action = 'vote'
candidate_id = -1
if flask.request.method == 'POST':
- next_action = ''
+ next_action = ''
form_values = flask.request.form.values()
if 'Submit' in form_values:
candidate_id = int(flask.request.form['candidate'])
@@ -372,10 +373,12 @@ def vote_simple(election_alias):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
- return flask.render_template('election/vote_simple.html', election=election,
- num_candidates=num_candidates,
- usernamemap=usernamemap,
- candidate_id=candidate_id, nextaction=next_action)
+ return flask.render_template(
+ 'election/vote_simple.html', election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ candidate_id=candidate_id, nextaction=next_action)
+
### AUTH VIEWS #############################################
@@ -437,7 +440,7 @@ def admin_new_election():
frontpage=form.frontpage.data,
voting_type=form.voting_type.data,
candidates_are_fasusers=form.candidates_are_fasusers.data,
- fas_user = flask.g.fas_user.username,
+ fas_user=flask.g.fas_user.username,
)
# Fix start_date and end_date to use datetime
@@ -625,4 +628,3 @@ def election_results(election_alias):
'election/results.html',
election=election,
usernamemap=usernamemap)
-
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
index 3afb452..bfa6d01 100644
--- a/fedora_elections/default_config.py
+++ b/fedora_elections/default_config.py
@@ -9,7 +9,7 @@ SECRET_KEY = 'change me'
FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
FAS_USERNAME = 'elections'
-FAS_PASSWORD ='el3ctMEn0w'
+FAS_PASSWORD = 'el3ctMEn0w'
FAS_CHECK_CERT = False
FAS_HTTPS_REQUIRED = False
diff --git a/fedora_elections/fedmsgshim.py b/fedora_elections/fedmsgshim.py
index 775f785..2e13398 100644
--- a/fedora_elections/fedmsgshim.py
+++ b/fedora_elections/fedmsgshim.py
@@ -1,6 +1,6 @@
""" This is a temporary shim that allows fedmsg to be an optional dependency of
-elections. If fedmsg is installed, these function calls will try to actually send
-messages. If it is not installed, it will return silently.
+elections. If fedmsg is installed, these function calls will try to actually
+send messages. If it is not installed, it will return silently.
:Author: Ralph Bean <rbean(a)redhat.com>
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 5b7d838..63a38c6 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -17,9 +17,9 @@ class ElectionForm(wtf.Form):
wtforms.validators.Length(max=100),
wtforms.validators.Regexp(
'[a-z0-9_-]+',
- message=('Alias may only contain lower case letters, numbers, '
- 'hyphens and underscores.')),
- ])
+ message=('Alias may only contain lower case letters, numbers, '
+ 'hyphens and underscores.')),
+ ])
description = wtforms.TextAreaField(
'Description', [
@@ -27,9 +27,9 @@ class ElectionForm(wtf.Form):
voting_type = wtforms.RadioField(
'Type',
- choices=[('range', 'Range Voting'),
- ('simple', 'Simple Voting')],
- default='range')
+ choices=[('range', 'Range Voting'),
+ ('simple', 'Simple Voting')],
+ default='range')
url = wtforms.TextField(
'URL', [
@@ -46,7 +46,7 @@ class ElectionForm(wtf.Form):
wtforms.validators.Required()])
number_elected = wtforms.IntegerField(
- 'Number elected',[
+ 'Number elected', [
wtforms.validators.Required(),
wtforms.validators.NumberRange(min=1)],
default=1)
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 1911eaa..113d814 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -74,9 +74,8 @@ def create_session(db_url, debug=False, pool_recycle=3600):
:return a Session that can be used to query the database.
"""
- engine = sa.create_engine(db_url,
- echo=debug,
- pool_recycle=pool_recycle)
+ engine = sa.create_engine(
+ db_url, echo=debug, pool_recycle=pool_recycle)
scopedsession = scoped_session(sessionmaker(bind=engine))
return scopedsession
@@ -188,7 +187,6 @@ class ElectionAdminGroup(BASE):
role_required = sa.Column(sa.Enum(u'user', u'sponsor', u'administrator'),
nullable=False)
-
@classmethod
def by_election_id(cls, session, election_id):
""" Return all the ElectionAdminGroup having a specific election_id.
@@ -200,7 +198,6 @@ class ElectionAdminGroup(BASE):
).all()
-
class Candidate(BASE):
__tablename__ = 'candidates'
@@ -209,8 +206,8 @@ class Candidate(BASE):
nullable=False)
election = relationship(
'Election', backref=backref('candidates', lazy='dynamic'))
- name = sa.Column(sa.Unicode(150), nullable=False) # FAS username if
- # candidates_are_fasusers
+ # FAS username if candidates_are_fasusers
+ name = sa.Column(sa.Unicode(150), nullable=False)
url = sa.Column(sa.Unicode(250))
@property
@@ -244,7 +241,7 @@ class Vote(BASE):
__table_args__ = (sa.UniqueConstraint('election_id', 'voter',
'candidate_id',
- name='eid_voter_cid'), {} )
+ name='eid_voter_cid'), {})
id = sa.Column(sa.Integer, primary_key=True)
election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
diff --git a/fedora_elections/redirect.py b/fedora_elections/redirect.py
index 5571abe..e7aa43e 100644
--- a/fedora_elections/redirect.py
+++ b/fedora_elections/redirect.py
@@ -7,7 +7,7 @@ def is_safe_url(target):
ref_url = urlparse(flask.request.host_url)
test_url = urlparse(urljoin(flask.request.host_url, target))
return test_url.scheme in ('http', 'https') and \
- ref_url.netloc == test_url.netloc
+ ref_url.netloc == test_url.netloc
def safe_redirect_back(next=None, fallback=('index', {})):
commit 3b53a1b93d1a99983285847786ef0ee7b477f8c1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Feb 28 10:04:14 2014 +0100
Reduce indentation
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index 20b0d7a..fafefee 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -1,26 +1,26 @@
{% extends "base.html" %}
{% block title %}Election administration{% endblock %}
{% block body %}
- <h2>Election administration</h2>
- <table>
+ <h2>Election administration</h2>
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ </tr>
+ {% for election in elections %}
<tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
+ <td>
+ <a href="{{ url_for('admin_view_election',
+ election_alias=election.alias) }}">
+ {{ election.summary }}
+ </a>
+ </td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
</tr>
- {% for election in elections %}
- <tr>
- <td>
- <a href="{{ url_for('admin_view_election',
- election_alias=election.alias) }}">
- {{ election.summary }}
- </a>
- </td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- </tr>
- {% endfor %}
- </table>
+ {% endfor %}
+ </table>
{%endblock %}
commit d8586b46f778d3a848e48400eb4c0bdf510cf14f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Feb 20 11:21:51 2014 +0100
Adjust the runserver script
diff --git a/runserver.py b/runserver.py
index 9c845ba..293b6d0 100644
--- a/runserver.py
+++ b/runserver.py
@@ -2,5 +2,5 @@ import __main__
__main__.__requires__ = ['SQLAlchemy >= 0.7']
import pkg_resources
-from fedora_elections import app
-app.run()
+from fedora_elections import APP
+APP.run()
commit 19ae57db972a8c0bd99b3a99b7baca7ad26c9e3b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:40:17 2014 +0100
Fix typo when retrieving a valid election
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 78285e3..5cf5e68 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -110,7 +110,7 @@ def get_valid_election(election_alias, ended=False):
return flask.redirect(flask.url_for(
'election_results', election_alias=election.alias))
- elif not ended and election.status == 'In progress':
+ elif ended and election.status == 'In progress':
flask.flash(
'Sorry but this election is in progress, you may not see its '
'results already.')
commit b655d21a06a76740e6a245da05d177db99395d3e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:12:53 2014 +0100
Fix retrieving a valid election that have ended
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index c350da7..78285e3 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -90,7 +90,7 @@ def election_admin_required(f):
return decorated_function
-def get_valid_election(election_alias):
+def get_valid_election(election_alias, ended=False):
""" Return the election if it is valid (no pending, not ended).
"""
election = models.Election.get(SESSION, alias=election_alias)
@@ -103,12 +103,19 @@ def get_valid_election(election_alias):
flask.flash('Voting has not yet started, sorry.')
return redirect.safe_redirect_back()
- elif election.status == 'Ended':
+ elif not ended and election.status == 'Ended':
flask.flash(
'This election is closed. You have been redirected to the '
'election results.')
return flask.redirect(flask.url_for(
'election_results', election_alias=election.alias))
+
+ elif not ended and election.status == 'In progress':
+ flask.flash(
+ 'Sorry but this election is in progress, you may not see its '
+ 'results already.')
+ return redirect.safe_redirect_back()
+
return election
@@ -574,23 +581,17 @@ def admin_delete_candidate(election_alias, candidate_id):
@APP.route('/results/<election_alias>')
def election_results(election_alias):
- election = get_valid_election(election_alias)
+ election = get_valid_election(election_alias, ended=True)
if not isinstance(election, models.Election):
return election
- if election.status == 'In progress':
- flask.flash(
- 'Sorry but this election is in progress, you may not see its '
- 'results already.')
- return redirect.safe_redirect_back()
-
elif election.embargoed == 1:
if not hasattr(flask.g, 'fas_user') or not flask.g.fas_user:
- flask.flash("We are sorry. The results for this election"
- "cannot be viewed because they are currently"
- " embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
+ flask.flash("We are sorry. The results for this election"
+ "cannot be viewed because they are currently"
+ " embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
else:
if APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
flask.g.fas_user.groups:
@@ -620,5 +621,8 @@ def election_results(election_alias):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
- return flask.render_template('election/results.html', election=election,
- usernamemap=usernamemap)
+ return flask.render_template(
+ 'election/results.html',
+ election=election,
+ usernamemap=usernamemap)
+
commit c1959e1a6e7014a4360a4958ee7db70c5d881942
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:59:05 2014 +0100
Bug fix in Vote.of_user_on_election() method to account for the count argument
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 48d34d6..1911eaa 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -273,4 +273,7 @@ class Vote(BASE):
cls.voter == user
)
- return query.all()
+ if count:
+ return query.count()
+ else:
+ return query.all()
commit defc5157a6c0ad03820ba4ae951ce6d1e059dbcb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:51:54 2014 +0100
Adjust requirements
diff --git a/requirements.txt b/requirements.txt
index 89a2349..336fbab 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,5 +4,5 @@ flask
kitchen
python-fedora
Flask-FAS
-Flask-sqlalchemy
Flask-wtf
+
diff --git a/setup.py b/setup.py
index c1ac01e..b05d9ba 100644
--- a/setup.py
+++ b/setup.py
@@ -12,5 +12,5 @@ setup( name = 'fedora-elections'
, packages = find_packages()
, zip_safe = False
, include_package_data = True
- , install_requires = ['Flask', 'SQLAlchemy>=0.7', 'flask-sqlalchemy']
+ , install_requires = ['Flask', 'SQLAlchemy>=0.7']
)
commit 2ea19c044a74c73fc521fb7730720e8023ebd8c5
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:51:47 2014 +0100
Add decorator when login is required
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index ea7203b..c350da7 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -183,6 +183,7 @@ def open_elections():
### ELECTION VIEWS #############################################
@APP.route('/vote/<election_alias>', methods=['GET', 'POST'])
+@login_required
def vote(election_alias):
election = get_valid_election(election_alias)
@@ -207,6 +208,7 @@ def vote(election_alias):
@APP.route('/vote_range/<election_alias>', methods=['GET', 'POST'])
+@login_required
def vote_range(election_alias):
election = get_valid_election(election_alias)
@@ -214,8 +216,8 @@ def vote_range(election_alias):
return election
if (election.voting_type == 'simple'):
- return flask.redirect(flask.url_for('vote_simple',
- election_alias=election_alias))
+ return flask.redirect(flask.url_for(
+ 'vote_simple', election_alias=election_alias))
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
@@ -302,6 +304,7 @@ def vote_range(election_alias):
nextaction=next_action)
@APP.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
+@login_required
def vote_simple(election_alias):
election = get_valid_election(election_alias)
@@ -309,8 +312,8 @@ def vote_simple(election_alias):
return election
if (election.voting_type == 'range'):
- return flask.redirect(flask.url_for('vote_range',
- election_alias=election_alias))
+ return flask.redirect(flask.url_for(
+ 'vote_range', election_alias=election_alias))
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
@@ -583,7 +586,7 @@ def election_results(election_alias):
return redirect.safe_redirect_back()
elif election.embargoed == 1:
- if flask.g.fas_user is None:
+ if not hasattr(flask.g, 'fas_user') or not flask.g.fas_user:
flask.flash("We are sorry. The results for this election"
"cannot be viewed because they are currently"
" embargoed pending formal announcement.")
commit 18ea8617ae2fd177d84ef8f87eeced5bcd74dfdf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:13:25 2014 +0100
Add script to easily create the database required to work on elections
diff --git a/createdb.py b/createdb.py
new file mode 100644
index 0000000..af15e2d
--- /dev/null
+++ b/createdb.py
@@ -0,0 +1,7 @@
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7']
+import pkg_resources
+
+from fedora_elections import APP
+from fedora_elections.models import create_tables
+create_tables(APP.config['DB_URL'], debug=True)
commit 742b4ad06fad990d938eb3baeacf9e177820ac4d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:12:59 2014 +0100
Adjust the tests to run on the current application
diff --git a/tests/base.py b/tests/base.py
index e12179b..c0e5c73 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -1,10 +1,13 @@
# -*- coding: utf-8 -*-
#
-# Copyright (C) 2012 Red Hat, Inc.
+# Copyright (C) 2012-2014 Red Hat, Inc.
#
# Author(s): Toshio Kuratomi <toshio(a)fedoraproject.org>
+import os
+
import fedora_elections
+from fedora_elections.models import create_tables
# Note: This is used in unittests that use nose's generator tests so we can't
# subclass unittest.TestCase
@@ -12,20 +15,45 @@ class BaseRequestTests(object):
'''Setup application for further testing of requests'''
def setUp(self):
- fedora_elections.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + '../tests/testing.db'
- fedora_elections.app.config['TESTING'] = True
- self.app = fedora_elections.app.test_client()
- fedora_elections.db.create_all()
+ cur_folder = os.path.dirname(os.path.realpath(__file__))
+ db_file = os.path.join(cur_folder, 'testing.sqlite')
+ print '* creating %s' % db_file
+ fedora_elections.APP.config['DB_URL'] = 'sqlite:///%s' % db_file
+
+ self.session = create_tables(fedora_elections.APP.config['DB_URL'])
+
+ fedora_elections.APP.config['TESTING'] = True
+ fedora_elections.APP.SESSION = self.session
+ self.app = fedora_elections.APP.test_client()
def tearDown(self):
- fedora_elections.db.session.remove()
- fedora_elections.db.drop_all()
+ cur_folder = os.path.dirname(os.path.realpath(__file__))
+ db_file = os.path.join(cur_folder, 'testing.sqlite')
+ print '* cleaning %s' % db_file
+ os.unlink(db_file)
+
url_subs = {'election_alias': None, 'candidate_id': None}
-anon_urls = ('/', '/archive', '/login', '/logout', '/vote/%(election_alias)s')
-authed_view_urls = ('/logout', '/vote/%(election_alias)s')
-admin_view_urls = ('/admin', '/admin/new', '/admin/%(election_alias)s')
-admin_modify_urls = ('/admin/new', '/admin/%(election_alias)s/edit',
+
+anon_urls = (
+ '/',
+ '/archive',
+ '/login',
+ '/logout',
+ '/vote/%(election_alias)s')
+
+authed_view_urls = (
+ '/logout',
+ '/vote/%(election_alias)s')
+
+admin_view_urls = (
+ '/admin',
+ '/admin/new',
+ '/admin/%(election_alias)s')
+
+admin_modify_urls = (
+ '/admin/new',
+ '/admin/%(election_alias)s/edit',
'/admin/%(election_alias)s/candidates/new',
'/admin/%(election_alias)s/candidates/%(candidate_id)d/edit',
'/admin/%(election_alias)s/candidates/%(candidate_id)d/delete')
diff --git a/tests/test_smoke.py b/tests/test_smoke.py
index 1d7b306..0fbb3d2 100644
--- a/tests/test_smoke.py
+++ b/tests/test_smoke.py
@@ -14,26 +14,37 @@ from nose import tools, with_setup
from base import BaseRequestTests, anon_urls, authed_view_urls, \
admin_view_urls, admin_modify_urls, url_subs
-from fedora_elections import db, models
-from fedora_elections import fas
+import fedora_elections
+import fedora_elections.models
class TestSmoke(BaseRequestTests):
'''Basic test that we don't get 500s'''
def setUp(self):
super(TestSmoke, self).setUp()
- fas.login = Mock(return_value=True)
+ fedora_elections.SESSION = self.session
+
+ fedora_elections.FAS.login = Mock(return_value=True)
# Add an election
- election = models.Election(summary='Test Election 1',
- alias='test1', description='lorem ipsum of course',
+ election = fedora_elections.models.Election(
+ summary='Test Election 1',
+ alias='test1',
+ description='lorem ipsum of course',
url='http://fedorahosted.org/elections',
start_date=datetime.datetime.utcnow(),
- end_date=datetime.datetime.utcnow()+datetime.timedelta(hours=1))
- db.session.add(election)
+ end_date=datetime.datetime.utcnow()+datetime.timedelta(hours=1),
+ fas_user='fchiulli',
+ )
+ self.session.add(election)
+
# Add candidate to election
- candidate = models.Candidate(election=election, name='Mr Testy', url='http://fedoraproject.org')
- db.session.add(candidate)
- db.session.commit()
+ candidate = fedora_elections.models.Candidate(
+ election=election,
+ name='Mr Testy',
+ url='http://fedoraproject.org')
+ self.session.add(candidate)
+
+ self.session.commit()
self.url_subs = copy(url_subs)
self.url_subs['election_alias'] = 'test1'
@@ -64,10 +75,13 @@ class TestSmoke(BaseRequestTests):
self.check_status_code(*args, **kwargs)
def test_all_url_subs(self):
- '''Testing that the testsuite is going to substitute all variables in the url.'''
+ '''Testing that the testsuite is going to substitute all variables
+ in the url
+ '''
for i in self.url_subs.values():
if i is None:
- raise Exception('Smoke test must be updated for new url variable present in base.py')
+ raise Exception('Smoke test must be updated for new url '
+ 'variable present in base.py')
def test_anon_view(self):
for url in anon_urls:
commit a4690735ca927226587220e64c5e971e9121bac7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:12:32 2014 +0100
We no longer have a development configuration file to use on the tests
diff --git a/testing b/testing
index 529e8ee..76f7c3e 100755
--- a/testing
+++ b/testing
@@ -1,2 +1,3 @@
#!/bin/sh
-FEDORA_ELECTIONS_CONFIG=../development.cfg nosetests $*
+nosetests $*
+
commit 8a46c6a1a7233e5f70c3496e05a2609fb8b277de
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:12:10 2014 +0100
Port the flask application away from flask-sqlalchemy
This allows us to have all the querying done in one place
and not tight coupling of the logic with this library.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index c6e5f21..ea7203b 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -21,11 +21,11 @@
# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
# Toshio Kuratomi <tkuratom(a)redhat.com>
# Frank Chiulli <fchiulli(a)fedoraproject.org>
+# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
import flask
from flask.ext.fas import FAS
-from flask.ext.sqlalchemy import SQLAlchemy
from fedora.client import AuthError, AppError
from fedora.client.fas2 import AccountSystem
@@ -38,30 +38,29 @@ from datetime import datetime, time
from functools import wraps
from urlparse import urlparse, urljoin
-app = flask.Flask(__name__)
-app.config.from_object('fedora_elections.default_config')
-app.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
-
-# set up SQLAlchemy
-db = SQLAlchemy(app)
+APP = flask.Flask(__name__)
+APP.config.from_object('fedora_elections.default_config')
+if APP.config.get('FEDORA_ELECTIONS_CONFIG') \
+ and os.path.exists(APP.config['FEDORA_ELECTIONS_CONFIG']):
+ APP.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
# set up FAS
-fas = FAS(app)
+FAS = FAS(APP)
# FAS for usernames.
-# baseURL = "https://admin.fedoraproject.org/accounts"
-# fasuser = "elections"
-# faspw = "elections"
-# fas2_url = 'http://fakefas.fedoraproject.org/accounts/'
-fas2_url = app.config['FAS_BASE_URL']
-fas2_username = app.config['FAS_USERNAME']
-fas2_password = app.config['FAS_PASSWORD']
-fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password,
- insecure=True)
+FAS2 = AccountSystem(
+ APP.config['FAS_BASE_URL'],
+ username=APP.config['FAS_USERNAME'],
+ password=APP.config['FAS_PASSWORD'],
+ insecure=APP.config['FAS_CHECK_CERT']
+)
# modular imports
-from fedora_elections import forms, models, redirect
+from fedora_elections import models
+SESSION = models.create_session(APP.config['DB_URL'])
+from fedora_elections import forms
+from fedora_elections import redirect
def remove_csrf(form_data):
@@ -84,29 +83,53 @@ def election_admin_required(f):
if flask.g.fas_user is None:
return flask.redirect(flask.url_for('auth_login',
next=flask.request.url))
- if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
+ if APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
flask.g.fas_user.groups:
flask.abort(403)
return f(*args, **kwargs)
return decorated_function
+def get_valid_election(election_alias):
+ """ Return the election if it is valid (no pending, not ended).
+ """
+ election = models.Election.get(SESSION, alias=election_alias)
+
+ if not election:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'Pending':
+ flask.flash('Voting has not yet started, sorry.')
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Ended':
+ flask.flash(
+ 'This election is closed. You have been redirected to the '
+ 'election results.')
+ return flask.redirect(flask.url_for(
+ 'election_results', election_alias=election.alias))
+ return election
+
+
### LIST VIEWS #############################################
-(a)app.route('/')
+(a)APP.route('/')
def index():
- elections = models.Election.query.filter_by(frontpage=True).all()
+ elections = models.Election.search(SESSION, frontpage=True)
num_elections = len(elections)
- return flask.render_template('list/index.html', elections=elections,
- num_elections=num_elections,
- title="Elections")
+ return flask.render_template(
+ 'list/index.html',
+ elections=elections,
+ num_elections=num_elections,
+ title="Elections")
-(a)app.route('/about/<election_alias>')
+(a)APP.route('/about/<election_alias>')
def about_election(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
+ election = models.Election.get(SESSION, alias=election_alias)
+
+ if not election:
flask.flash('The election, %s, does not exist.' % election_alias)
return redirect.safe_redirect_back()
@@ -120,115 +143,84 @@ def about_election(election_alias):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
- return flask.render_template('election/about.html', election=election,
- usernamemap=usernamemap)
+ return flask.render_template(
+ 'election/about.html',
+ election=election,
+ usernamemap=usernamemap)
-(a)app.route('/archive')
+(a)APP.route('/archive')
def archived_elections():
now = datetime.utcnow()
- try:
- elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date<now, models.Election.id>0).all()
- except NoResultFound:
- flask.flash('There are no archived elections.')
- return redirect.safe_redirect_back()
+ elections = models.Election.get_older_election(SESSION, now)
- num_elections = len(elections)
- if (num_elections == 0):
- flask.flash('There are no open elections.')
+ if not elections:
+ flask.flash('There are no archived elections.')
return redirect.safe_redirect_back()
- return flask.render_template('election/archive.html',
- num_elections=num_elections,
- elections=elections)
+ return flask.render_template(
+ 'election/archive.html',
+ elections=elections)
-(a)app.route('/open')
+(a)APP.route('/open')
def open_elections():
now = datetime.utcnow()
- try:
- elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date>now,
- models.Election.id>0).all()
- except NoResultFound:
- flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
+ elections = models.Election.get_open_election(SESSION, now)
- num_elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date>now,
- models.Election.id>0).count()
- if (num_elections == 0):
+ if not elections:
flask.flash('There are no open elections.')
return redirect.safe_redirect_back()
- return flask.render_template('list/index.html', elections=elections,
- num_elections=num_elections,
- title='Open Elections')
+ return flask.render_template(
+ 'list/index.html',
+ elections=elections,
+ title='Open Elections')
### ELECTION VIEWS #############################################
-(a)app.route('/vote/<election_alias>', methods=['GET', 'POST'])
+(a)APP.route('/vote/<election_alias>', methods=['GET', 'POST'])
def vote(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
+ election = get_valid_election(election_alias)
- if election.status == 'Pending':
- flask.flash('Voting has not yet started, sorry.')
- return redirect.safe_redirect_back()
+ if not isinstance(election, models.Election):
+ return election
- elif election.status == 'Ended':
- flask.flash('This election is closed. You have been redirected to ' +
- 'the election results.')
- return flask.redirect(flask.url_for('election_results',
- election_alias=election.alias))
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, election.id, count=True)
- votes = models.Vote.query.filter_by(election_id=election.id,
- voter=flask.g.fas_user.username).count()
- if (votes != 0):
+ if votes > 0:
flask.flash('You have already voted in the election!')
return redirect.safe_redirect_back()
- if (election.voting_type == 'range'):
+ if election.voting_type == 'range':
return vote_range(election_alias)
- elif (election.voting_type == 'simple'):
+ elif election.voting_type == 'simple':
return vote_simple(election_alias)
else:
- flask.flash('Unknown election voting type: %s' % election.voting_type)
+ flask.flash(
+ 'Unknown election voting type: %s' % election.voting_type)
return redirect.safe_redirect_back()
-(a)app.route('/vote_range/<election_alias>', methods=['GET', 'POST'])
+(a)APP.route('/vote_range/<election_alias>', methods=['GET', 'POST'])
def vote_range(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
+ election = get_valid_election(election_alias)
- if election.status == 'Pending':
- flask.flash('Voting has not yet started, sorry.')
- return redirect.safe_redirect_back()
-
- elif election.status == 'Ended':
- flask.flash('This election is closed. You have been redirected to ' +
- 'the election results.')
- return flask.redirect(flask.url_for('election_results',
- election_alias=election.alias))
+ if not isinstance(election, models.Election):
+ return election
if (election.voting_type == 'simple'):
return flask.redirect(flask.url_for('vote_simple',
election_alias=election_alias))
- votes = models.Vote.query.filter_by(election_id=election.id,
- voter=flask.g.fas_user.username).count()
- if (votes != 0):
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, election.id, count=True)
+
+ if votes > 0:
flask.flash('You have already voted in the election!')
return redirect.safe_redirect_back()
@@ -257,13 +249,15 @@ def vote_range(election_alias):
return redirect.safe_redirect_back()
for uvote in uvotes:
- new_vote = models.Vote(election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=uvote,
- value=uvotes[uvote] )
- db.session.add(new_vote)
- db.session.commit()
+ new_vote = models.Vote(
+ election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=uvote,
+ value=uvotes[uvote]
+ )
+ SESSION.add(new_vote)
+ SESSION.commit()
flask.flash("Your vote has been recorded. Thank you!")
return redirect.safe_redirect_back()
@@ -299,35 +293,28 @@ def vote_range(election_alias):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
- return flask.render_template('election/vote_range.html', election=election,
- num_candidates=num_candidates,
- usernamemap=usernamemap,
- voteinfo=uvotes, nextaction=next_action)
+ return flask.render_template(
+ 'election/vote_range.html',
+ election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ voteinfo=uvotes,
+ nextaction=next_action)
-(a)app.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
+(a)APP.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
def vote_simple(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
+ election = get_valid_election(election_alias)
- if election.status == 'Pending':
- flask.flash('Voting has not yet started, sorry.')
- return redirect.safe_redirect_back()
-
- elif election.status == 'Ended':
- flask.flash('This election is closed. You have been redirected to ' +
- 'the election results.')
- return flask.redirect(flask.url_for('election_results',
- election_alias=election_alias))
+ if not isinstance(election, models.Election):
+ return election
if (election.voting_type == 'range'):
return flask.redirect(flask.url_for('vote_range',
election_alias=election_alias))
- votes = models.Vote.query.filter_by(election_id=election.id,
- voter=flask.g.fas_user.username).count()
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, election.id, count=True)
+
if (votes != 0):
flask.flash('You have already voted in the election!')
return redirect.safe_redirect_back()
@@ -341,13 +328,15 @@ def vote_simple(election_alias):
form_values = flask.request.form.values()
if 'Submit' in form_values:
candidate_id = int(flask.request.form['candidate'])
- new_vote = models.Vote(election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=candidate_id,
- value=1 )
- db.session.add(new_vote)
- db.session.commit()
+ new_vote = models.Vote(
+ election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=candidate_id,
+ value=1
+ )
+ SESSION.add(new_vote)
+ SESSION.commit()
flask.flash("Your vote has been recorded. Thank you!")
return redirect.safe_redirect_back()
@@ -380,13 +369,13 @@ def vote_simple(election_alias):
### AUTH VIEWS #############################################
-(a)app.route('/login', methods=('GET', 'POST'))
+(a)APP.route('/login', methods=('GET', 'POST'))
def auth_login():
if flask.g.fas_user:
return redirect.safe_redirect_back()
form = forms.LoginForm()
if form.validate_on_submit():
- if fas.login(form.username.data, form.password.data):
+ if FAS.login(form.username.data, form.password.data):
flask.flash('Welcome, %s' % flask.g.fas_user.username)
return redirect.safe_redirect_back()
else:
@@ -394,7 +383,7 @@ def auth_login():
return flask.render_template('auth/login.html', form=form)
-(a)app.route('/logout')
+(a)APP.route('/logout')
def auth_logout():
if not flask.g.fas_user:
return redirect.safe_redirect_back()
@@ -405,176 +394,217 @@ def auth_logout():
### ELECTION ADMIN VIEWS ###################################
-(a)app.route('/admin/')
+(a)APP.route('/admin/')
@election_admin_required
def admin_view_elections():
- elections = models.Election.query.\
- filter_by(fas_user=flask.g.fas_user.username).all()
- num_elections = len(elections)
- if (num_elections == 0):
+ elections = models.Election.search(
+ SESSION, fas_user=flask.g.fas_user.username)
+
+ if not elections:
flask.flash('You do not have any elections.')
return redirect.safe_redirect_back()
- return flask.render_template('admin/all_elections.html',
- num_elections=num_elections,
- elections=elections)
+ return flask.render_template(
+ 'admin/all_elections.html',
+ elections=elections)
-(a)app.route('/admin/new', methods=('GET', 'POST'))
+(a)APP.route('/admin/new', methods=('GET', 'POST'))
@election_admin_required
def admin_new_election():
form = forms.ElectionForm()
if form.validate_on_submit():
- election = models.Election(**remove_csrf(form.data))
+
+ election = models.Election(
+ summary=form.summary.data,
+ alias=form.alias.data,
+ description=form.description.data,
+ url=form.url.data,
+ start_date=form.start_date.data,
+ end_date=form.end_date.data,
+ number_elected=form.number_elected.data,
+ embargoed=form.embargoed.data,
+ frontpage=form.frontpage.data,
+ voting_type=form.voting_type.data,
+ candidates_are_fasusers=form.candidates_are_fasusers.data,
+ fas_user = flask.g.fas_user.username,
+ )
+
# Fix start_date and end_date to use datetime
election.start_date = datetime.combine(election.start_date, time())
election.end_date = datetime.combine(election.end_date,
time(23, 59, 59))
- election.fas_user = flask.g.fas_user.username
- db.session.add(election)
- admin = models.ElectionAdminGroup(election=election,
- group_name=app.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
- role_required=u'user')
- db.session.add(admin)
- db.session.commit()
+ SESSION.add(election)
+
+ admin = models.ElectionAdminGroup(
+ election=election,
+ group_name=APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ role_required=u'user')
+ SESSION.add(admin)
+
+ SESSION.commit()
+
flask.flash('Election "%s" added' % election.summary)
fedmsgshim.publish(topic="election.new", msg=election)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/election_form.html', form=form,
- submit_text='Create election')
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+ return flask.render_template(
+ 'admin/election_form.html',
+ form=form,
+ submit_text='Create election')
-(a)app.route('/admin/<election_alias>/')
+(a)APP.route('/admin/<election_alias>/')
@election_admin_required
def admin_view_election(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
+ election = models.Election.get(SESSION, alias=election_alias)
if not election:
flask.abort(404)
- return flask.render_template('admin/view_election.html', election=election)
+
+ return flask.render_template(
+ 'admin/view_election.html',
+ election=election)
-(a)app.route('/admin/<election_alias>/edit', methods=('GET', 'POST'))
+(a)APP.route('/admin/<election_alias>/edit', methods=('GET', 'POST'))
@election_admin_required
def admin_edit_election(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
+ election = models.Election.get(SESSION, alias=election_alias)
if not election:
flask.abort(404)
+
form = forms.ElectionForm(election.id, obj=election)
if form.validate_on_submit():
form.populate_obj(election)
- db.session.commit()
+ SESSION.commit()
flask.flash('Election "%s" saved' % election.summary)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/election_form.html', form=form,
- submit_text='Edit election')
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+ return flask.render_template(
+ 'admin/election_form.html',
+ form=form,
+ submit_text='Edit election')
-(a)app.route('/admin/<election_alias>/candidates/new', methods=('GET', 'POST'))
+
+(a)APP.route('/admin/<election_alias>/candidates/new', methods=('GET', 'POST'))
@election_admin_required
def admin_add_candidate(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
+ election = models.Election.get(SESSION, alias=election_alias)
if not election:
flask.abort(404)
+
form = forms.CandidateForm()
if form.validate_on_submit():
- candidate = models.Candidate(election=election,
- **remove_csrf(form.data))
- db.session.add(candidate)
- db.session.commit()
+ candidate = models.Candidate(
+ election=election,
+ name=form.name.data,
+ url=form.url.data
+ )
+
+ SESSION.add(candidate)
+ SESSION.commit()
flask.flash('Candidate "%s" saved' % candidate.name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/candidate.html', form=form,
- submit_text='Add candidate')
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+ return flask.render_template(
+ 'admin/candidate.html',
+ form=form,
+ submit_text='Add candidate')
-(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
+
+(a)APP.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
methods=('GET', 'POST'))
@election_admin_required
def admin_edit_candidate(election_alias, candidate_id):
- election = models.Election.query.filter_by(alias=election_alias).first()
- candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ election = models.Election.get(SESSION, alias=election_alias)
+ candidate = models.Candidate.get(SESSION, candidate_id=candidate_id)
+
if not election or not candidate:
flask.abort(404)
+
if candidate.election != election:
flask.abort(404)
+
form = forms.CandidateForm(obj=candidate)
if form.validate_on_submit():
form.populate_obj(candidate)
- db.session.commit()
+ SESSION.commit()
flask.flash('Candidate "%s" saved' % candidate.name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/candidate.html', form=form,
- submit_text='Edit candidate')
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+
+ return flask.render_template(
+ 'admin/candidate.html',
+ form=form,
+ submit_text='Edit candidate')
-(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
+(a)APP.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
methods=('GET', 'POST'))
@election_admin_required
def admin_delete_candidate(election_alias, candidate_id):
- election = models.Election.query.filter_by(alias=election_alias).first()
- candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ election = models.Election.get(SESSION, alias=election_alias)
+ candidate = models.Candidate.get(SESSION, candidate_id=candidate_id)
+
if not election or not candidate:
flask.abort(404)
+
if candidate.election != election:
flask.abort(404)
+
form = forms.ConfirmationForm()
if form.validate_on_submit():
- db.session.delete(candidate)
candidate_name = candidate.name
- db.session.commit()
+ SESSION.delete(candidate)
+ SESSION.commit()
flask.flash('Candidate "%s" deleted' % candidate_name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/delete_candidate.html', form=form,
- candidate=candidate)
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+ return flask.render_template(
+ 'admin/delete_candidate.html',
+ form=form,
+ candidate=candidate)
-(a)app.route('/results/<election_alias>')
+
+(a)APP.route('/results/<election_alias>')
def election_results(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
+ election = get_valid_election(election_alias)
- if election.status == 'In progress':
- flask.flash("We are sorry. The results for this election cannot be" \
- " viewed at this time because the election is still in" \
- " progress.")
- return redirect.safe_redirect_back()
+ if not isinstance(election, models.Election):
+ return election
- elif election.status == 'Pending':
- flask.flash("We are sorry. The results for this election cannot be" \
- " viewed at this time because the election has not" \
- " started.")
+ if election.status == 'In progress':
+ flask.flash(
+ 'Sorry but this election is in progress, you may not see its '
+ 'results already.')
return redirect.safe_redirect_back()
elif election.embargoed == 1:
if flask.g.fas_user is None:
- flask.flash("We are sorry. The results for this election" \
- "cannot be viewed because they are currently" \
+ flask.flash("We are sorry. The results for this election"
+ "cannot be viewed because they are currently"
" embargoed pending formal announcement.")
return redirect.safe_redirect_back()
else:
- if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
- flask.g.fas_user.groups:
+ if APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
+ flask.g.fas_user.groups:
pass
else:
match = 0
- admingroups = models.ElectionAdminGroup.query.filter_by(
- election_id=election.id).all()
+ admingroups = models.ElectionAdminGroup.by_election_id(
+ SESSION, election_id=election.id)
for admingroup in admingroups:
- if admingroup.group_name in flask.g.fas_user.groups:
+ if admingroup.group_name in flask.g.fas_user.groups:
match = 1
if match == 0:
- flask.flash("We are sorry. The results for this election" \
- "cannot be viewed because they are currently" \
- " embargoed pending formal announcement.")
+ flask.flash(
+ "We are sorry. The results for this election"
+ "cannot be viewed because they are currently"
+ " embargoed pending formal announcement.")
return redirect.safe_redirect_back()
usernamemap = {}
commit 0ca5eebd59c929ec79b493df85810aaa5362f198
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:09:38 2014 +0100
Rework the models.py file to port it to plain SQLAlchemy
Expend the classes as well so that all the querying is done
only in this file an nowhere else in the project
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index b19bd33..48d34d6 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -1,31 +1,111 @@
-from fedora_elections import app, db
+# -*- coding: utf-8 -*-
+
+
+import sqlalchemy as sa
+from sqlalchemy import create_engine
+from sqlalchemy.ext.associationproxy import association_proxy
+from sqlalchemy.ext.compiler import compiles
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import scoped_session
+from sqlalchemy.orm import relation, relationship
+from sqlalchemy.orm import backref
+from sqlalchemy.orm.collections import attribute_mapped_collection
+from sqlalchemy.orm.collections import mapped_collection
+from sqlalchemy.orm.exc import NoResultFound
+from sqlalchemy.sql import and_
+from sqlalchemy.sql.expression import Executable, ClauseElement
+
+BASE = declarative_base()
from datetime import datetime
-class Election(db.Model):
+
+def create_tables(db_url, alembic_ini=None, debug=False):
+ """ Create the tables in the database using the information from the
+ url obtained.
+
+ :arg db_url, URL used to connect to the database. The URL contains
+ information with regards to the database engine, the host to
+ connect to, the user and password and the database name.
+ ie: <engine>://<user>:<password>@<host>/<dbname>
+ :kwarg alembic_ini, path to the alembic ini file. This is necessary
+ to be able to use alembic correctly, but not for the unit-tests.
+ :kwarg debug, a boolean specifying wether we should have the verbose
+ output of sqlalchemy or not.
+ :return a session that can be used to query the database.
+
+ """
+ engine = create_engine(db_url, echo=debug)
+ BASE.metadata.create_all(engine)
+ #engine.execute(collection_package_create_view(driver=engine.driver))
+ if db_url.startswith('sqlite:'):
+ ## Ignore the warning about con_record
+ # pylint: disable=W0613
+ def _fk_pragma_on_connect(dbapi_con, con_record):
+ ''' Tries to enforce referential constraints on sqlite. '''
+ dbapi_con.execute('pragma foreign_keys=ON')
+ sa.event.listen(engine, 'connect', _fk_pragma_on_connect)
+
+ if alembic_ini is not None: # pragma: no cover
+ # then, load the Alembic configuration and generate the
+ # version table, "stamping" it with the most recent rev:
+
+ ## Ignore the warning missing alembic
+ # pylint: disable=F0401
+ from alembic.config import Config
+ from alembic import command
+ alembic_cfg = Config(alembic_ini)
+ command.stamp(alembic_cfg, "head")
+
+ scopedsession = scoped_session(sessionmaker(bind=engine))
+ return scopedsession
+
+
+def create_session(db_url, debug=False, pool_recycle=3600):
+ """ Create the Session object to use to query the database.
+
+ :arg db_url: URL used to connect to the database. The URL contains
+ information with regards to the database engine, the host to connect
+ to, the user and password and the database name.
+ ie: <engine>://<user>:<password>@<host>/<dbname>
+ :kwarg debug: a boolean specifying wether we should have the verbose
+ output of sqlalchemy or not.
+ :return a Session that can be used to query the database.
+
+ """
+ engine = sa.create_engine(db_url,
+ echo=debug,
+ pool_recycle=pool_recycle)
+ scopedsession = scoped_session(sessionmaker(bind=engine))
+ return scopedsession
+
+
+class Election(BASE):
__tablename__ = 'elections'
- id = db.Column(db.Integer, primary_key=True)
- summary = db.Column(db.Unicode(150), unique=True, nullable=False)
- alias = db.Column(db.Unicode(100), unique=True, nullable=False)
- description = db.Column(db.UnicodeText, nullable=False)
- url = db.Column(db.Unicode(250), nullable=False)
- start_date = db.Column(db.DateTime, nullable=False)
- end_date = db.Column(db.DateTime, nullable=False)
- number_elected = db.Column(db.Integer, nullable=False, default=1)
- embargoed = db.Column(db.Boolean, nullable=False, default=True)
- frontpage = db.Column(db.Boolean, nullable=False, default=False)
- voting_type = db.Column(db.Unicode(100), nullable=False, default=u'range')
- candidates_are_fasusers = db.Column(db.Boolean, nullable=False,
+ id = sa.Column(sa.Integer, primary_key=True)
+ summary = sa.Column(sa.Unicode(150), unique=True, nullable=False)
+ alias = sa.Column(sa.Unicode(100), unique=True, nullable=False)
+ description = sa.Column(sa.UnicodeText, nullable=False)
+ url = sa.Column(sa.Unicode(250), nullable=False)
+ start_date = sa.Column(sa.DateTime, nullable=False)
+ end_date = sa.Column(sa.DateTime, nullable=False)
+ number_elected = sa.Column(sa.Integer, nullable=False, default=1)
+ embargoed = sa.Column(sa.Boolean, nullable=False, default=True)
+ frontpage = sa.Column(sa.Boolean, nullable=False, default=False)
+ voting_type = sa.Column(sa.Unicode(100), nullable=False, default=u'range')
+ candidates_are_fasusers = sa.Column(sa.Boolean, nullable=False,
default=False)
- fas_user = db.Column(db.Unicode(50), nullable=False)
+ fas_user = sa.Column(sa.Unicode(50), nullable=False)
@property
def status(self):
- if datetime.now() < self.start_date:
+ now = datetime.utcnow()
+ if now < self.start_date:
return 'Pending'
else:
- if datetime.now() < self.end_date:
+ if now < self.end_date:
return 'In progress'
else:
if self.embargoed:
@@ -37,67 +117,160 @@ class Election(db.Model):
def locked(self):
return datetime.now() >= self.start_date
+ @classmethod
+ def search(cls, session, frontpage=None, alias=None, summary=None,
+ fas_user=None):
+ """ Search the election and filter based on the arguments passed.
+ """
+ query = session.query(cls)
+
+ if frontpage is not None:
+ query = query.filter(cls.frontpage == frontpage)
+
+ if alias is not None:
+ query = query.filter(cls.alias == alias)
-class ElectionAdminGroup(db.Model):
+ if summary is not None:
+ query = query.filter(cls.summary == summary)
+
+ if fas_user is not None:
+ query = query.filter(cls.fas_user == fas_user)
+
+ return query.all()
+
+ @classmethod
+ def by_alias(cls, session, alias):
+ """ Return a specific election based on its alias. """
+ return session.query(cls).filter(cls.alias == alias).first()
+
+ get = by_alias
+
+ @classmethod
+ def get_older_election(cls, session, limit):
+ """ Return all the election which end_date if older than the
+ provided limit.
+ """
+ query = session.query(
+ cls
+ ).filter(
+ cls.end_date < limit
+ ).order_by(
+ cls.start_date
+ )
+
+ return query.all()
+
+ @classmethod
+ def get_open_election(cls, session, limit):
+ """ Return all the election which end_date if greater or equal to
+ the provided limit.
+ """
+ query = session.query(
+ cls
+ ).filter(
+ cls.end_date >= limit
+ ).order_by(
+ cls.start_date
+ )
+
+ return query.all()
+
+
+class ElectionAdminGroup(BASE):
__tablename__ = 'electionadmingroups'
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ id = sa.Column(sa.Integer, primary_key=True)
+ election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
nullable=False)
- election = db.relationship('Election', backref=db.backref('admin_groups',
- lazy='dynamic'))
- group_name = db.Column(db.Unicode(150), nullable=False)
- role_required = db.Column(db.Enum(u'user', u'sponsor', u'administrator'),
+ election = relationship(
+ 'Election', backref=backref('admin_groups', lazy='dynamic'))
+ group_name = sa.Column(sa.Unicode(150), nullable=False)
+ role_required = sa.Column(sa.Enum(u'user', u'sponsor', u'administrator'),
nullable=False)
-class Candidate(db.Model):
+ @classmethod
+ def by_election_id(cls, session, election_id):
+ """ Return all the ElectionAdminGroup having a specific election_id.
+ """
+ return session.query(
+ cls
+ ).filter(
+ cls.election_id == election_id
+ ).all()
+
+
+
+class Candidate(BASE):
__tablename__ = 'candidates'
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ id = sa.Column(sa.Integer, primary_key=True)
+ election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
nullable=False)
- election = db.relationship('Election', backref=db.backref('candidates',
- lazy='dynamic'))
- name = db.Column(db.Unicode(150), nullable=False) # FAS username if
+ election = relationship(
+ 'Election', backref=backref('candidates', lazy='dynamic'))
+ name = sa.Column(sa.Unicode(150), nullable=False) # FAS username if
# candidates_are_fasusers
- url = db.Column(db.Unicode(250))
+ url = sa.Column(sa.Unicode(250))
@property
def vote_count(self):
values = [v.value for v in self.votes]
return sum(values)
+ @classmethod
+ def by_id(cls, session, candidate_id):
+ """ Return a specific election based on its alias. """
+ return session.query(cls).filter(cls.id == candidate_id).first()
-class LegalVoter(db.Model):
+ get = by_id
+
+
+class LegalVoter(BASE):
__tablename__ = 'legalvoters'
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ id = sa.Column(sa.Integer, primary_key=True)
+ election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
nullable=False)
- election = db.relationship('Election', backref=db.backref('legal_voters',
- lazy='dynamic'))
+ election = relationship(
+ 'Election', backref=backref('legal_voters', lazy='dynamic'))
# special names:
# "cla + one" = cla_done + 1 non-cla group
- group_name = db.Column(db.Unicode(150), nullable=False)
+ group_name = sa.Column(sa.Unicode(150), nullable=False)
-class Vote(db.Model):
+class Vote(BASE):
__tablename__ = 'votes'
- __table_args__ = (db.UniqueConstraint('election_id', 'voter',
+ __table_args__ = (sa.UniqueConstraint('election_id', 'voter',
'candidate_id',
name='eid_voter_cid'), {} )
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ id = sa.Column(sa.Integer, primary_key=True)
+ election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
nullable=False)
- election = db.relationship('Election', backref=db.backref('votes',
- lazy='dynamic'))
- voter = db.Column(db.Unicode(150), nullable=False)
- timestamp = db.Column(db.DateTime, nullable=False)
- candidate_id = db.Column(db.Integer, db.ForeignKey('candidates.id'),
+ election = relationship(
+ 'Election', backref=backref('votes', lazy='dynamic'))
+ voter = sa.Column(sa.Unicode(150), nullable=False)
+ timestamp = sa.Column(sa.DateTime, nullable=False)
+ candidate_id = sa.Column(sa.Integer, sa.ForeignKey('candidates.id'),
nullable=False)
- candidate = db.relationship('Candidate', backref=db.backref('votes',
- lazy='dynamic'))
- value = db.Column(db.Integer, nullable=False)
+ candidate = relationship(
+ 'Candidate', backref=backref('votes', lazy='dynamic'))
+ value = sa.Column(sa.Integer, nullable=False)
+
+ @classmethod
+ def of_user_on_election(cls, session, user, election_id, count=False):
+ """ Return the votes of a user on a specific election.
+ If count if True, then return the number of votes instead of the
+ actual votes.
+ """
+ query = session.query(
+ cls
+ ).filter(
+ cls.election_id == election_id
+ ).filter(
+ cls.voter == user
+ )
+
+ return query.all()
commit 1ddffc428f5415f2e0a6cd395673046bccae807e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:08:42 2014 +0100
We cannot have 0 elections, this is taken care of in the controller
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index ae210e0..b25a469 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -1,6 +1,5 @@
{% extends "base.html" %}
{% block body %}
- {% if num_elections > 0 %}
<h2>{{ title }}</h2>
<table>
<tr>
@@ -32,5 +31,4 @@
</tr>
{% endfor %}
</table>
- {% endif %}
{% endblock %}
commit 748c938561ef7da13941a9cafbad0a5391193a71
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:08:39 2014 +0100
We cannot have 0 elections, this is taken care of in the controller
diff --git a/fedora_elections/templates/election/archive.html b/fedora_elections/templates/election/archive.html
index 8740b3c..9874dd5 100644
--- a/fedora_elections/templates/election/archive.html
+++ b/fedora_elections/templates/election/archive.html
@@ -1,28 +1,26 @@
{% extends "base.html" %}
{% block body %}
- {% if num_elections > 0 %}
- <h2>Past Elections</h2>
- <table border="1" cellpadding="1">
+ <h2>Past Elections</h2>
+ <table border="1" cellpadding="1">
+ <tr>
+ <th></th>
+ <th>Start Date (UTC)</th>
+ <th>End Date (UTC)</th>
+ <th></th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
<tr>
- <th></th>
- <th>Start Date (UTC)</th>
- <th>End Date (UTC)</th>
- <th></th>
- <th></th>
+ <td>{{election.description}}</td>
+ <td>{{election.start_date}}</td>
+ <td>{{election.end_date}}</td>
+ <td>
+ <a href="about/{{ election.alias }}">More information</a>
+ </td>
+ <td>
+ <a href="results/{{ election.alias }}">Results</a>
+ </td>
</tr>
- {% for election in elections %}
- <tr>
- <td>{{election.description}}</td>
- <td>{{election.start_date}}</td>
- <td>{{election.end_date}}</td>
- <td>
- <a href="about/{{ election.alias }}"">More information</a>
- </td>
- <td>
- <a href="results/{{ election.alias }}">Results</a>
- </td>
- </tr>
- {% endfor %}
- </table>
- {% endif %}
+ {% endfor %}
+ </table>
{% endblock %}
commit 54b5bfaf7c03aaee66e89671370cebe9b463b783
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:08:32 2014 +0100
We cannot have 0 elections, this is taken care of in the controller
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index e168ac0..20b0d7a 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -1,7 +1,6 @@
{% extends "base.html" %}
{% block title %}Election administration{% endblock %}
{% block body %}
- {% if num_elections > 0 %}
<h2>Election administration</h2>
<table>
<tr>
@@ -24,5 +23,4 @@
</tr>
{% endfor %}
</table>
- {% endif %}
{%endblock %}
commit 0a3939782f9e8c6a64887ff67dfdc9eb87983da8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:08:15 2014 +0100
Remove queries from the form
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 8fddf23..5b7d838 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -1,6 +1,7 @@
from flask.ext import wtf
import wtforms
+from fedora_elections import SESSION
from fedora_elections.models import Election
@@ -62,7 +63,7 @@ class ElectionForm(wtf.Form):
form._election_id = election_id
def validate_summary(form, field):
- check = Election.query.filter_by(summary=form.summary.data).all()
+ check = Election.search(SESSION, summary=form.summary.data)
if check:
if not (form._election_id and form._election_id == check[0].id):
raise wtforms.ValidationError(
@@ -72,7 +73,7 @@ class ElectionForm(wtf.Form):
if form.alias.data == 'new':
raise wtforms.ValidationError(flask.Markup(
'The alias cannot be <code>new</code>.'))
- check = Election.query.filter_by(alias=form.alias.data).all()
+ check = Election.search(SESSION, alias=form.alias.data)
if check:
if not (form._election_id and form._election_id == check[0].id):
raise wtforms.ValidationError(
commit 1c2bab83521827b9e442d5343d52bd0de86e42ee
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 13:22:49 2014 +0100
Remove un-necessary configuraiton file now that everything is moved to the default_config
diff --git a/development.cfg b/development.cfg
deleted file mode 100644
index e4b83bb..0000000
--- a/development.cfg
+++ /dev/null
@@ -1,18 +0,0 @@
-import os, os.path
-
-DEBUG = True
-
-
-# You will want to change this for your install
-SECRET_KEY = 'change me'
-SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.getcwd(), 'database.sqlite')
-
-FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
-FAS_USERNAME = 'elections'
-FAS_PASSWORD ='el3ctMEn0w'
-FAS_CHECK_CERT = False
-FAS_HTTPS_REQUIRED = False
-
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
-# List of URLs for additional site-wide CSS
-EXTRA_CSS = []
diff --git a/development.cfg.sample b/development.cfg.sample
deleted file mode 100644
index 1dff33a..0000000
--- a/development.cfg.sample
+++ /dev/null
@@ -1,18 +0,0 @@
-import os, os.path
-
-DEBUG = True
-
-
-# You will want to change this for your install
-SECRET_KEY = 'change me'
-SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.getcwd(), 'database.sqlite')
-
-FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
-FAS_USERNAME = 'elections'
-FAS_PASSWORD ='fake_fas_password'
-FAS_CHECK_CERT = False
-FAS_HTTPS_REQUIRED = False
-
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
-# List of URLs for additional site-wide CSS
-EXTRA_CSS = []
diff --git a/fedora_elections/fedora-elections.cfg.in b/fedora_elections/fedora-elections.cfg.in
deleted file mode 100644
index 4b49118..0000000
--- a/fedora_elections/fedora-elections.cfg.in
+++ /dev/null
@@ -1,18 +0,0 @@
-import os, os.path
-
-DEBUG = True
-
-
-# You will want to change this for your install
-SECRET_KEY = 'change me'
-SQLALCHEMY_DATABASE_URI = 'sqlite:///@SHAREDSTDIR@/fedora_elections/database.sqlite'
-
-FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
-FAS_USERNAME = 'fcelections'
-FAS_PASSWORD = 'el3cTo$hozer'
-FAS_CHECK_CERT = False
-FAS_FLASK_COOKIE_REQUIRES_HTTPS = False
-
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
-# List of URLs for additional site-wide CSS
-EXTRA_CSS = []
commit f7db6051a929c4b3b803275f473c7086806b0727
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 13:21:54 2014 +0100
Populate the default config with the values present in the sample development configuration file
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
index 7b5b794..3afb452 100644
--- a/fedora_elections/default_config.py
+++ b/fedora_elections/default_config.py
@@ -1 +1,18 @@
FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+
+# url to the database server:
+DB_URL = 'sqlite:////var/tmp/elections_dev.sqlite'
+
+
+# You will want to change this for your install
+SECRET_KEY = 'change me'
+
+FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
+FAS_USERNAME = 'elections'
+FAS_PASSWORD ='el3ctMEn0w'
+FAS_CHECK_CERT = False
+FAS_HTTPS_REQUIRED = False
+
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+# List of URLs for additional site-wide CSS
+EXTRA_CSS = []
commit 4adfcbf2e1280ea51bf8695a7a69363c380dcdd7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 13:21:20 2014 +0100
Remove the sample createdb.in and the corresponding python script
diff --git a/fedora_elections/createdb.in b/fedora_elections/createdb.in
deleted file mode 100644
index 46fabad..0000000
--- a/fedora_elections/createdb.in
+++ /dev/null
@@ -1 +0,0 @@
-FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora-elections/fedora-elections.cfg python @DATADIR(a)/fedora_elections/createdb.py
diff --git a/fedora_elections/createdb.py b/fedora_elections/createdb.py
deleted file mode 100644
index a909c5e..0000000
--- a/fedora_elections/createdb.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7']
-import pkg_resources
-
-from fedora_elections import db
-db.create_all()
commit ee3b59733095b3dd757b94652b1705275b0b5e4a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 13:07:48 2014 +0100
Do not run on port 80 by default as this will conflict with any apache server running
diff --git a/runserver.py b/runserver.py
index b9103e0..9c845ba 100644
--- a/runserver.py
+++ b/runserver.py
@@ -3,4 +3,4 @@ __main__.__requires__ = ['SQLAlchemy >= 0.7']
import pkg_resources
from fedora_elections import app
-app.run(host='0.0.0.0', port=80)
+app.run()
commit ac8ecdaec2a4b30752583cfce4e79b65e0f4f730
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 13:01:49 2014 +0100
Fix the forms to use directly wtforms instead of flask-wtf
There has been a release of flask-wtf that stopped providing the interface to wtforms.
This commit allows us to deal with these changes in flask-wtf
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 0f769a2..8fddf23 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -1,36 +1,61 @@
from flask.ext import wtf
+import wtforms
from fedora_elections.models import Election
class ElectionForm(wtf.Form):
- summary = wtf.TextField('Summary', [wtf.validators.Required(),
- wtf.validators.Length(max=150)])
- alias = wtf.TextField('Alias', [
- wtf.validators.Required(),
- wtf.validators.Length(max=100),
- wtf.validators.Regexp('[a-z0-9_-]+',
- message=('Alias may only contain lower case '
- 'letters, numbers, hyphens and '
- 'underscores.')),
+ summary = wtforms.TextField(
+ 'Summary', [
+ wtforms.validators.Required(),
+ wtforms.validators.Length(max=150)])
+
+ alias = wtforms.TextField(
+ 'Alias', [
+ wtforms.validators.Required(),
+ wtforms.validators.Length(max=100),
+ wtforms.validators.Regexp(
+ '[a-z0-9_-]+',
+ message=('Alias may only contain lower case letters, numbers, '
+ 'hyphens and underscores.')),
])
- description = wtf.TextAreaField('Description', [wtf.validators.Required()])
- voting_type = wtf.RadioField('Type',
- choices=[('range', 'Range Voting'),
- ('simple', 'Simple Voting')],
- default='range')
- url = wtf.TextField('URL', [wtf.validators.Required(),
- wtf.validators.URL(),
- wtf.validators.Length(max=250)])
- start_date = wtf.DateField('Start date', [wtf.validators.Required()])
- end_date = wtf.DateField('End date', [wtf.validators.Required()])
- number_elected = wtf.IntegerField('Number elected',
- [wtf.validators.Required(),
- wtf.validators.NumberRange(min=1)],
- default=1)
- candidates_are_fasusers = wtf.BooleanField('Candidates are FAS users?')
- frontpage = wtf.BooleanField('Show on front page?')
- embargoed = wtf.BooleanField('Embargo results?', default=True)
+
+ description = wtforms.TextAreaField(
+ 'Description', [
+ wtforms.validators.Required()])
+
+ voting_type = wtforms.RadioField(
+ 'Type',
+ choices=[('range', 'Range Voting'),
+ ('simple', 'Simple Voting')],
+ default='range')
+
+ url = wtforms.TextField(
+ 'URL', [
+ wtforms.validators.Required(),
+ wtforms.validators.URL(),
+ wtforms.validators.Length(max=250)])
+
+ start_date = wtforms.DateField(
+ 'Start date', [
+ wtforms.validators.Required()])
+
+ end_date = wtforms.DateField(
+ 'End date', [
+ wtforms.validators.Required()])
+
+ number_elected = wtforms.IntegerField(
+ 'Number elected',[
+ wtforms.validators.Required(),
+ wtforms.validators.NumberRange(min=1)],
+ default=1)
+
+ candidates_are_fasusers = wtforms.BooleanField(
+ 'Candidates are FAS users?')
+
+ frontpage = wtforms.BooleanField('Show on front page?')
+
+ embargoed = wtforms.BooleanField('Embargo results?', default=True)
def __init__(form, election_id=None, *args, **kwargs):
super(ElectionForm, form).__init__(*args, **kwargs)
@@ -40,33 +65,35 @@ class ElectionForm(wtf.Form):
check = Election.query.filter_by(summary=form.summary.data).all()
if check:
if not (form._election_id and form._election_id == check[0].id):
- raise wtf.ValidationError('There is already another election '
- 'with this summary.')
+ raise wtforms.ValidationError(
+ 'There is already another election with this summary.')
def validate_alias(form, field):
if form.alias.data == 'new':
- raise wtf.ValidationError(flask.Markup('The alias cannot be '
- '<code>new</code>.'))
+ raise wtforms.ValidationError(flask.Markup(
+ 'The alias cannot be <code>new</code>.'))
check = Election.query.filter_by(alias=form.alias.data).all()
if check:
if not (form._election_id and form._election_id == check[0].id):
- raise wtf.ValidationError('There is already another election '
- 'with this alias.')
+ raise wtforms.ValidationError(
+ 'There is already another election with this alias.')
def validate_end_date(form, field):
if form.end_date.data <= form.start_date.data:
- raise wtf.ValidationError('End date must be later than '
- 'start date.')
+ raise wtforms.ValidationError(
+ 'End date must be later than start date.')
class CandidateForm(wtf.Form):
- name = wtf.TextField('Name', [wtf.validators.Required()])
- url = wtf.TextField('URL', [wtf.validators.Length(max=250)])
+ name = wtforms.TextField('Name', [wtforms.validators.Required()])
+ url = wtforms.TextField('URL', [wtforms.validators.Length(max=250)])
class LoginForm(wtf.Form):
- username = wtf.TextField('Username', [wtf.validators.Required()])
- password = wtf.PasswordField('Password', [wtf.validators.Required()])
+ username = wtforms.TextField(
+ 'Username', [wtforms.validators.Required()])
+ password = wtforms.PasswordField(
+ 'Password', [wtforms.validators.Required()])
class ConfirmationForm(wtf.Form):
commit d2d4b3b016000e41ad701cc9b9e9643ca7e191e7
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:27:14 2013 +0000
Updates for new version
diff --git a/fedora-elections.spec b/fedora-elections.spec
index 61a39ac..4977cb3 100644
--- a/fedora-elections.spec
+++ b/fedora-elections.spec
@@ -4,7 +4,7 @@
%define eggname fedora_elections
Name: fedora-elections
-Version: 0.1
+Version: 0.2
Release: 1%{?dist}
Summary: Fedora elections application
@@ -70,7 +70,8 @@ fedora-elections is the Fedora Elections application.
%prep
%setup -q
-./configure.py
+cd fedora_elections
+../configure.py
%build
@@ -83,24 +84,27 @@ rm -rf $RPM_BUILD_ROOT
%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT
%{__mkdir_p} %{buildroot}%{_sysconfdir}/%{name}
-%{__install} -m 444 %{name}.cfg %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
+%{__install} -m 444 fedora_elections/%{name}.cfg \
+ %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
%{__mkdir_p} %{buildroot}%{_sysconfdir}/httpd/conf.d
-%{__install} -m 444 httpd-fedora-elections.conf \
+%{__install} -m 444 fedora_elections/httpd-fedora-elections.conf \
%{buildroot}%{_sysconfdir}/httpd/conf.d/httpd-fedora-elections.conf
-%{__mkdir_p} %{buildroot}%{_datadir}/%{name}
-%{__install} -m 544 createdb %{buildroot}%{_datadir}/%{name}/createdb
-%{__install} -m 544 createdb.py %{buildroot}%{_datadir}/%{name}/createdb.py
-%{__install} -m 444 %{name}.wsgi %{buildroot}%{_datadir}/%{name}/%{name}.wsgi
+%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}
+%{__install} -m 544 fedora_elections/createdb %{buildroot}%{_datadir}/%{modname}/createdb
+%{__install} -m 544 fedora_elections/createdb.py \
+ %{buildroot}%{_datadir}/%{modname}/createdb.py
+%{__install} -m 444 fedora_elections/%{name}.wsgi \
+ %{buildroot}%{_datadir}/%{modname}/%{name}.wsgi
-%{__mkdir_p} %{buildroot}%{_datadir}/%{name}/static
-%{__install} -m 444 fedora_elections/static/* %{buildroot}%{_datadir}/%{name}/static
+%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/static
+%{__install} -m 444 fedora_elections/static/* %{buildroot}%{_datadir}/%{modname}/static
-%{__mkdir_p} %{buildroot}%{_datadir}/%{name}/images
-%{__install} -m 444 fedora_elections/images/* %{buildroot}%{_datadir}/%{name}/images
+%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/images
+%{__install} -m 444 fedora_elections/images/* %{buildroot}%{_datadir}/%{modname}/images
-%{__mkdir_p} %{buildroot}%{_sharedstatedir}/%{name}
+%{__mkdir_p} %{buildroot}%{_sharedstatedir}/%{modname}
%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates
%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/admin
@@ -127,9 +131,8 @@ rm -rf $RPM_BUILD_ROOT
%defattr(-,root,root,-)
%doc
%dir %{_sysconfdir}/%{name}
-%dir %{_sharedstatedir}/%{name}
-%attr(744,apache,apache) %{_sharedstatedir}/%{name}
-%{_datadir}/%{name}/
+%attr(744,apache,apache) %{_sharedstatedir}/%{modname}
+%{_datadir}/%{modname}/
%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
%config(noreplace) %{_sysconfdir}/httpd/conf.d/httpd-%{name}.conf
%{python_sitelib}/%{modname}/
@@ -137,14 +140,14 @@ rm -rf $RPM_BUILD_ROOT
%post
-cd %{_sharedstatedir}/%{name}
+cd %{_sharedstatedir}/%{modname}
if [[ ! -f database.sqlite ]]
then
#
# The database does not exist create one.
#
echo " Creating a database."
- %{_datadir}/%{name}/createdb
+ %{_datadir}/%{modname}/createdb
echo " Changing permissions."
chmod 644 database.sqlite
echo " Changing owner."
@@ -153,7 +156,7 @@ fi
%postun
-cd %{_sharedstatedir}/%{name}
+cd %{_sharedstatedir}/%{modname}
if [[ -f database.sqlite ]]
then
#
@@ -164,12 +167,15 @@ then
fi
cd ${_sharedstatedir}
-if [[ -d %{_sharedstatedir}/%{name} ]]
+if [[ -d %{_sharedstatedir}/%{modname} ]]
then
- /bin/rmdir %{_sharedstatedir}/%{name}
+ /bin/rmdir %{_sharedstatedir}/%{modname}
fi
%changelog
+* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.2
+- Simple voting
+
* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1
- Creation
commit 26afff3ffbdd0f0cd53ff18141512a1115f2b51a
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:26:17 2013 +0000
Update directory path
diff --git a/fedora_elections/fedora-elections.wsgi.in b/fedora_elections/fedora-elections.wsgi.in
index ecdccc9..430cc5c 100644
--- a/fedora_elections/fedora-elections.wsgi.in
+++ b/fedora_elections/fedora-elections.wsgi.in
@@ -11,7 +11,7 @@ import sys
sys.stdout = sys.stderr
## Set the environment variable pointing to the configuration file
-os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora_elections/fedora-elections.cfg'
+os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
## The following is only needed if you did not install
## as a python module (for example if you run it from a git clone).
commit 37da55e6f768ee1395d6e162a2c191b2c5f532ef
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:25:36 2013 +0000
Update directory path
diff --git a/fedora_elections/createdb.in b/fedora_elections/createdb.in
index e921d8a..46fabad 100644
--- a/fedora_elections/createdb.in
+++ b/fedora_elections/createdb.in
@@ -1 +1 @@
-FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora_elections/fedora-elections.cfg python @DATADIR(a)/fedora_elections/createdb.py
+FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora-elections/fedora-elections.cfg python @DATADIR(a)/fedora_elections/createdb.py
commit 339ee8189509eb65c2184855abece849df001322
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:24:36 2013 +0000
Update version
diff --git a/setup.py b/setup.py
index 83a6354..c1ac01e 100644
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ import pkg_resources
from setuptools import setup, find_packages
setup( name = 'fedora-elections'
- , version = '0.1'
+ , version = '0.2'
, author = 'Frank Chiulli'
, author_email = 'fchiulli(a)fedoraproject.org'
, packages = find_packages()
commit aeea827b5e845a2a847209a99a19cd304a6008f0
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:24:12 2013 +0000
Update task list
diff --git a/TODO b/TODO
index 9573c62..bffc060 100644
--- a/TODO
+++ b/TODO
@@ -3,10 +3,7 @@ Things that need doing to get this deployed
* Add legalvoters to an election
* Write a conversion script for the old db model to the new one
-* rpmify
* rpmify flask-fas (or get flask-fas into python-fedora)
-* Implement a voting method for range voting
-* Implement a voting method for non-range voting
* CSRF protection -- anything going through wtf forms is protected. It's up to
us to make sure that no data changing requests are coded without wtf forms.
commit e4c7f7ae7c2afe2f478687739e1c39f6433ee15c
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:23:57 2013 +0000
Add info to clone git repo
diff --git a/README b/README
index c768ad2..4ab94e0 100644
--- a/README
+++ b/README
@@ -47,6 +47,8 @@ Get the source code
You can obtain the code from github:
https://github.com/fedora-infra/elections.git
+git clone https://github.com/fedora-infra/elections.git
+
Configure the application
-------------------------
commit 90ca09bdfb7b28b7d399a240ea01fe4f0e078b7f
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Mon Sep 2 21:00:46 2013 +0000
More simple voting updates
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 86bfdc6..c6e5f21 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -186,7 +186,45 @@ def vote(election_alias):
elif election.status == 'Ended':
flask.flash('This election is closed. You have been redirected to ' +
'the election results.')
- return flask.redirect(flask.url_for('results', election.shortname))
+ return flask.redirect(flask.url_for('election_results',
+ election_alias=election.alias))
+
+ votes = models.Vote.query.filter_by(election_id=election.id,
+ voter=flask.g.fas_user.username).count()
+ if (votes != 0):
+ flask.flash('You have already voted in the election!')
+ return redirect.safe_redirect_back()
+
+ if (election.voting_type == 'range'):
+ return vote_range(election_alias)
+ elif (election.voting_type == 'simple'):
+ return vote_simple(election_alias)
+ else:
+ flask.flash('Unknown election voting type: %s' % election.voting_type)
+ return redirect.safe_redirect_back()
+
+
+(a)app.route('/vote_range/<election_alias>', methods=['GET', 'POST'])
+def vote_range(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'Pending':
+ flask.flash('Voting has not yet started, sorry.')
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Ended':
+ flask.flash('This election is closed. You have been redirected to ' +
+ 'the election results.')
+ return flask.redirect(flask.url_for('election_results',
+ election_alias=election.alias))
+
+ if (election.voting_type == 'simple'):
+ return flask.redirect(flask.url_for('vote_simple',
+ election_alias=election_alias))
votes = models.Vote.query.filter_by(election_id=election.id,
voter=flask.g.fas_user.username).count()
@@ -251,7 +289,6 @@ def vote(election_alias):
if (next_action != 'vote'):
next_action = 'confirm'
-
usernamemap = {}
if (election.candidates_are_fasusers):
for candidate in election.candidates:
@@ -262,11 +299,84 @@ def vote(election_alias):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
- return flask.render_template('election/vote.html', election=election,
+ return flask.render_template('election/vote_range.html', election=election,
num_candidates=num_candidates,
usernamemap=usernamemap,
voteinfo=uvotes, nextaction=next_action)
+(a)app.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
+def vote_simple(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'Pending':
+ flask.flash('Voting has not yet started, sorry.')
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Ended':
+ flask.flash('This election is closed. You have been redirected to ' +
+ 'the election results.')
+ return flask.redirect(flask.url_for('election_results',
+ election_alias=election_alias))
+
+ if (election.voting_type == 'range'):
+ return flask.redirect(flask.url_for('vote_range',
+ election_alias=election_alias))
+
+ votes = models.Vote.query.filter_by(election_id=election.id,
+ voter=flask.g.fas_user.username).count()
+ if (votes != 0):
+ flask.flash('You have already voted in the election!')
+ return redirect.safe_redirect_back()
+
+ num_candidates = election.candidates.count()
+ next_action = 'vote'
+ candidate_id = -1
+
+ if flask.request.method == 'POST':
+ next_action = ''
+ form_values = flask.request.form.values()
+ if 'Submit' in form_values:
+ candidate_id = int(flask.request.form['candidate'])
+ new_vote = models.Vote(election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=candidate_id,
+ value=1 )
+ db.session.add(new_vote)
+ db.session.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return redirect.safe_redirect_back()
+
+ elif 'Preview' in form_values:
+ if ('candidate' in flask.request.form):
+ flask.flash("Please confirm your vote!")
+ candidate_id = int(flask.request.form['candidate'])
+ else:
+ flask.flash("Please vote for a candidate")
+ next_action = 'vote'
+
+ if (next_action != 'vote'):
+ next_action = 'confirm'
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/vote_simple.html', election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ candidate_id=candidate_id, nextaction=next_action)
### AUTH VIEWS #############################################
@@ -310,7 +420,7 @@ def admin_view_elections():
elections=elections)
-(a)app.route('/admin/new', methods=("GET", "POST"))
+(a)app.route('/admin/new', methods=('GET', 'POST'))
@election_admin_required
def admin_new_election():
form = forms.ElectionForm()
@@ -344,7 +454,7 @@ def admin_view_election(election_alias):
return flask.render_template('admin/view_election.html', election=election)
-(a)app.route('/admin/<election_alias>/edit', methods=("GET", "POST"))
+(a)app.route('/admin/<election_alias>/edit', methods=('GET', 'POST'))
@election_admin_required
def admin_edit_election(election_alias):
election = models.Election.query.filter_by(alias=election_alias).first()
@@ -361,7 +471,7 @@ def admin_edit_election(election_alias):
submit_text='Edit election')
-(a)app.route('/admin/<election_alias>/candidates/new', methods=("GET", "POST"))
+(a)app.route('/admin/<election_alias>/candidates/new', methods=('GET', 'POST'))
@election_admin_required
def admin_add_candidate(election_alias):
election = models.Election.query.filter_by(alias=election_alias).first()
@@ -381,7 +491,7 @@ def admin_add_candidate(election_alias):
@app.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
- methods=("GET", "POST"))
+ methods=('GET', 'POST'))
@election_admin_required
def admin_edit_candidate(election_alias, candidate_id):
election = models.Election.query.filter_by(alias=election_alias).first()
@@ -402,7 +512,7 @@ def admin_edit_candidate(election_alias, candidate_id):
@app.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
- methods=("GET", "POST"))
+ methods=('GET', 'POST'))
@election_admin_required
def admin_delete_candidate(election_alias, candidate_id):
election = models.Election.query.filter_by(alias=election_alias).first()
diff --git a/fedora_elections/templates/election/vote.html b/fedora_elections/templates/election/vote.html
deleted file mode 100644
index e3c4dd4..0000000
--- a/fedora_elections/templates/election/vote.html
+++ /dev/null
@@ -1,116 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-<h2>{{election.summary}}</h2>
-
-<p>{{election.description}}</p>
-
-<p><a href="{{election.url}}">[More Information]</a></p>
-
-<form action="" method="post">
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- <a href="{{candidate.url}}">[Info]</a>
- </td>
- <!-- If user is logged in: -->
- {% if g.fas_user %}
- {% if nextaction == 'vote' %}
- <td>
- <select name="{{candidate.id}}">
- {% for opt in range(0, num_candidates+1) %}
- <option value={{opt}}>{{opt}}</option>
- {% endfor %}
- </select>
- </td>
- {% endif %}
- {% if nextaction == 'confirm' %}
- <td>
- {% set cid = candidate.id %}
- <input type="hidden" name="{{candidate.id}}"
- value="{{voteinfo[cid]}}" />
- {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
- {% if weightpercent >= 90 %}
- <div class="vthot">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 70 %}
- <div class="vtwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 50 %}
- <div class="vtmedwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 30 %}
- <div class="vtmedcool">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 10 %}
- <div class="vtcool">
- {{voteinfo[cid]}}
- </div>
- {% else %}
- <div class="vtcold">
- {{voteinfo[cid]}}
- </div>
- {% endif %}
- </td>
- {% endif %}
- {% endif %}
- <!-- End if user logged in -->
- </tr>
- {% endfor %}
- </table>
- {% if g.fas_user %}
- <p>
- {% if nextaction == 'confirm' %}
- <input type="submit" name="{{nextaction}}" value="Submit" />
- {% else %}
- <input type="submit" name="{{nextaction}}" value="Preview" />
- {% endif %}
- </p>
- {% endif %}
-</form>
-
-<!-- Make this conditional on whether the user is logged in and can vote in the election -->
-{% if election.legal_voters.count() != 0 %}
-<p>
- To vote in this election you must be a member of any one of the following groups:
-</p>
-<ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
-</ul>
-{% else %}
-<p />
-{% endif %}
-<div>
- <h2>Information on Range Voting</h2>
- <p>
- Fedora Project has implemented
- <a href="http://rangevoting.org/">Range Voting</a>
- for this election, in particular the "Range (score-summing, blanks
- treated as zero score, no quorum rule)" range voting system.
- </p>
- <p>
- To cast your vote in this election simply select a value between 0 and
- {{num_candidates}} with 0 as 'least or no preference' and
- {{num_candidates}} as 'highest preference'.
- </p>
- <p>
- At the end of the election, the highest ranking candidate(s) are marked
- as the winners.
- </p>
- <p>
- For more information about Range Voting, visit the
- <a href="http://rangevoting.org/">Center for Range Voting</a>.
- </p>
-</div>
-{% endblock %}
diff --git a/fedora_elections/templates/election/vote_range.html b/fedora_elections/templates/election/vote_range.html
new file mode 100644
index 0000000..e3b2e84
--- /dev/null
+++ b/fedora_elections/templates/election/vote_range.html
@@ -0,0 +1,116 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{election.summary}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
+<form action="" method="post">
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ <a href="{{candidate.url}}">[Info]</a>
+ </td>
+ <!-- If user is logged in: -->
+ {% if g.fas_user %}
+ {% if nextaction == 'vote' %}
+ <td>
+ <select name="{{candidate.id}}">
+ {% for opt in range(0, num_candidates+1) %}
+ <option value={{opt}}>{{opt}}</option>
+ {% endfor %}
+ </select>
+ </td>
+ {% endif %}
+ {% if nextaction == 'confirm' %}
+ <td>
+ {% set cid = candidate.id %}
+ <input type="hidden" name="{{candidate.id}}"
+ value="{{voteinfo[cid]}}" />
+ {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
+ {% if weightpercent >= 90 %}
+ <div class="vthot">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 70 %}
+ <div class="vtwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 50 %}
+ <div class="vtmedwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 30 %}
+ <div class="vtmedcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 10 %}
+ <div class="vtcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% else %}
+ <div class="vtcold">
+ {{voteinfo[cid]}}
+ </div>
+ {% endif %}
+ </td>
+ {% endif %}
+ {% endif %}
+ <!-- End if user logged in -->
+ </tr>
+ {% endfor %}
+ </table>
+ {% if g.fas_user %}
+ <p>
+ {% if nextaction == 'confirm' %}
+ <input type="submit" name="{{nextaction}}" value="Submit" />
+ {% else %}
+ <input type="submit" name="{{nextaction}}" value="Preview" />
+ {% endif %}
+ </p>
+ {% endif %}
+</form>
+
+<!-- Make this conditional on whether the user is logged in and can vote in the election -->
+{% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+{% else %}
+ <p />
+{% endif %}
+<div>
+ <h2>Information on Range Voting</h2>
+ <p>
+ Fedora Project has implemented
+ <a href="http://rangevoting.org/">Range Voting</a>
+ for this election, in particular the "Range (score-summing, blanks
+ treated as zero score, no quorum rule)" range voting system.
+ </p>
+ <p>
+ To cast your vote in this election simply select a value between 0 and
+ {{num_candidates}} with 0 as 'least or no preference' and
+ {{num_candidates}} as 'highest preference'.
+ </p>
+ <p>
+ At the end of the election, the highest ranking candidate(s) are marked
+ as the winners.
+ </p>
+ <p>
+ For more information about Range Voting, visit the
+ <a href="http://rangevoting.org/">Center for Range Voting</a>.
+ </p>
+</div>
+{% endblock %}
diff --git a/fedora_elections/templates/election/vote_simple.html b/fedora_elections/templates/election/vote_simple.html
new file mode 100644
index 0000000..17434a1
--- /dev/null
+++ b/fedora_elections/templates/election/vote_simple.html
@@ -0,0 +1,53 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{election.summary}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
+<form action="" method="post">
+ <table border="0" cellpadding="1">
+ {% for candidate in election.candidates|sort %}
+ <tr>
+ <td>
+ {% if candidate.id == candidate_id %}
+ <input type="radio" name="candidate" value="{{ candidate.id }}" checked="checked">
+ {% else %}
+ <input type="radio" name="candidate" value="{{ candidate.id }}">
+ {% endif %}
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ <a href="{{candidate.url}}">[Info]</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% if g.fas_user %}
+ <p>
+ {% if nextaction == 'confirm' %}
+ <input type="submit" name="{{nextaction}}" value="Submit" />
+ {% else %}
+ <input type="submit" name="{{nextaction}}" value="Preview" />
+ {% endif %}
+ </p>
+ {% endif %}
+</form>
+
+<!-- Make this conditional on whether the user is logged in and can vote in the election -->
+{% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+{% else %}
+ <p />
+{% endif %}
+{% endblock %}
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 290c2cf..ae210e0 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -19,8 +19,12 @@
<td>
{% if election.status == "In progress" %}
{% if g.fas_user %}
- <a href="{{ url_for('vote', election_alias=election.alias) }}">
- Vote now!
+ {% if election.voting_type == 'range' %}
+ <a href="{{ url_for('vote_range', election_alias=election.alias) }}">
+ {% elif election.voting_type == 'simple' %}
+ <a href="{{ url_for('vote_simple', election_alias=election.alias) }}">
+ {% endif %}
+ Vote now!
</a>
{% endif %}
{% endif %}
commit 075463be8bb5afef58b083dc96b90d7e1e29b158
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Aug 25 20:21:10 2013 +0000
Create simple vote election
diff --git a/README b/README
index 442ebb5..c768ad2 100644
--- a/README
+++ b/README
@@ -50,7 +50,7 @@ You can obtain the code from github:
Configure the application
-------------------------
-cd fedora-elections
+cd fedora_elections
../configure.py --install-conf=${PWD%/*} --install-data=${PWD%/*} --shared-state=${PWD%/*}
This will create createdb, fedora-elections.cfg, fedora-elections.wsgi and
diff --git a/fedora-elections/__init__.py b/fedora-elections/__init__.py
deleted file mode 100644
index 86bfdc6..0000000
--- a/fedora-elections/__init__.py
+++ /dev/null
@@ -1,481 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2012 Red Hat, Inc.
-# Copyright © 2012 Ian Weller <ianweller(a)fedoraproject.org>
-# Copyright © 2012 Toshio Kuratomi <tkuratom(a)redhat.com>
-# Copyright © 2012 Frank Chiulli <fchiulli(a)fedoraproject.org>
-#
-# This copyrighted material is made available to anyone wishing to use, modify,
-# copy, or redistribute it subject to the terms and conditions of the GNU
-# General Public License v.2, or (at your option) any later version. This
-# program is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY expressed or implied, including the implied warranties of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
-# Public License for more details. You should have received a copy of the GNU
-# General Public License along with this program; if not, write to the Free
-# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
-# code or documentation are not subject to the GNU General Public License and
-# may only be used or replicated with the express permission of Red Hat, Inc.
-#
-# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
-# Toshio Kuratomi <tkuratom(a)redhat.com>
-# Frank Chiulli <fchiulli(a)fedoraproject.org>
-#
-
-import flask
-from flask.ext.fas import FAS
-from flask.ext.sqlalchemy import SQLAlchemy
-
-from fedora.client import AuthError, AppError
-from fedora.client.fas2 import AccountSystem
-
-from sqlalchemy.orm.exc import NoResultFound
-
-import fedmsgshim
-
-from datetime import datetime, time
-from functools import wraps
-from urlparse import urlparse, urljoin
-
-app = flask.Flask(__name__)
-app.config.from_object('fedora_elections.default_config')
-app.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
-
-# set up SQLAlchemy
-db = SQLAlchemy(app)
-
-# set up FAS
-fas = FAS(app)
-
-# FAS for usernames.
-# baseURL = "https://admin.fedoraproject.org/accounts"
-# fasuser = "elections"
-# faspw = "elections"
-# fas2_url = 'http://fakefas.fedoraproject.org/accounts/'
-fas2_url = app.config['FAS_BASE_URL']
-fas2_username = app.config['FAS_USERNAME']
-fas2_password = app.config['FAS_PASSWORD']
-fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password,
- insecure=True)
-
-
-# modular imports
-from fedora_elections import forms, models, redirect
-
-
-def remove_csrf(form_data):
- return dict([(k, v) for k, v in form_data.items() if k != 'csrf'])
-
-
-def login_required(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- if flask.g.fas_user is None:
- return flask.redirect(flask.url_for('auth_login',
- next=flask.request.url))
- return f(*args, **kwargs)
- return decorated_function
-
-
-def election_admin_required(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- if flask.g.fas_user is None:
- return flask.redirect(flask.url_for('auth_login',
- next=flask.request.url))
- if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
- flask.g.fas_user.groups:
- flask.abort(403)
- return f(*args, **kwargs)
- return decorated_function
-
-
-### LIST VIEWS #############################################
-
-(a)app.route('/')
-def index():
- elections = models.Election.query.filter_by(frontpage=True).all()
- num_elections = len(elections)
- return flask.render_template('list/index.html', elections=elections,
- num_elections=num_elections,
- title="Elections")
-
-
-(a)app.route('/about/<election_alias>')
-def about_election(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template('election/about.html', election=election,
- usernamemap=usernamemap)
-
-
-(a)app.route('/archive')
-def archived_elections():
- now = datetime.utcnow()
-
- try:
- elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date<now, models.Election.id>0).all()
- except NoResultFound:
- flask.flash('There are no archived elections.')
- return redirect.safe_redirect_back()
-
- num_elections = len(elections)
- if (num_elections == 0):
- flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
-
- return flask.render_template('election/archive.html',
- num_elections=num_elections,
- elections=elections)
-
-
-(a)app.route('/open')
-def open_elections():
- now = datetime.utcnow()
-
- try:
- elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date>now,
- models.Election.id>0).all()
- except NoResultFound:
- flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
-
- num_elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date>now,
- models.Election.id>0).count()
- if (num_elections == 0):
- flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
-
- return flask.render_template('list/index.html', elections=elections,
- num_elections=num_elections,
- title='Open Elections')
-
-
-### ELECTION VIEWS #############################################
-
-(a)app.route('/vote/<election_alias>', methods=['GET', 'POST'])
-def vote(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
-
- if election.status == 'Pending':
- flask.flash('Voting has not yet started, sorry.')
- return redirect.safe_redirect_back()
-
- elif election.status == 'Ended':
- flask.flash('This election is closed. You have been redirected to ' +
- 'the election results.')
- return flask.redirect(flask.url_for('results', election.shortname))
-
- votes = models.Vote.query.filter_by(election_id=election.id,
- voter=flask.g.fas_user.username).count()
- if (votes != 0):
- flask.flash('You have already voted in the election!')
- return redirect.safe_redirect_back()
-
- num_candidates = election.candidates.count()
-
- uvotes = {}
- for candidate in election.candidates:
- uvotes[candidate.id] = ""
- next_action = 'vote'
-
- if flask.request.method == 'POST':
- next_action = ''
- form_values = flask.request.form.values()
- if 'Submit' in form_values:
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote >= 0 and vote <= num_candidates:
- uvotes[candidate.id] = int(vote)
- else:
- flask.flash("Invalid Ballot!")
- return redirect.safe_redirect_back()
-
- except ValueError:
- flask.flash("Invalid Ballot!")
- return redirect.safe_redirect_back()
-
- for uvote in uvotes:
- new_vote = models.Vote(election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=uvote,
- value=uvotes[uvote] )
- db.session.add(new_vote)
- db.session.commit()
-
- flask.flash("Your vote has been recorded. Thank you!")
- return redirect.safe_redirect_back()
-
- elif 'Preview' in form_values:
- flask.flash("Please confirm your vote!")
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote > num_candidates:
- flask.flash("Invalid data.")
- uvotes[candidate.id] = 0
- next_action = 'vote'
- elif vote >= 0:
- uvotes[candidate.id] = vote
- else:
- flask.flash("Invalid data2.")
- uvotes[c.id] = 0
- next_action = 'vote'
- except ValueError:
- flask.flash("Invalid data")
-
- if (next_action != 'vote'):
- next_action = 'confirm'
-
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template('election/vote.html', election=election,
- num_candidates=num_candidates,
- usernamemap=usernamemap,
- voteinfo=uvotes, nextaction=next_action)
-
-
-### AUTH VIEWS #############################################
-
-(a)app.route('/login', methods=('GET', 'POST'))
-def auth_login():
- if flask.g.fas_user:
- return redirect.safe_redirect_back()
- form = forms.LoginForm()
- if form.validate_on_submit():
- if fas.login(form.username.data, form.password.data):
- flask.flash('Welcome, %s' % flask.g.fas_user.username)
- return redirect.safe_redirect_back()
- else:
- flask.flash('Incorrect username or password')
- return flask.render_template('auth/login.html', form=form)
-
-
-(a)app.route('/logout')
-def auth_logout():
- if not flask.g.fas_user:
- return redirect.safe_redirect_back()
- fas.logout()
- flask.flash('You have been logged out')
- return redirect.safe_redirect_back()
-
-
-### ELECTION ADMIN VIEWS ###################################
-
-(a)app.route('/admin/')
-@election_admin_required
-def admin_view_elections():
- elections = models.Election.query.\
- filter_by(fas_user=flask.g.fas_user.username).all()
- num_elections = len(elections)
- if (num_elections == 0):
- flask.flash('You do not have any elections.')
- return redirect.safe_redirect_back()
-
- return flask.render_template('admin/all_elections.html',
- num_elections=num_elections,
- elections=elections)
-
-
-(a)app.route('/admin/new', methods=("GET", "POST"))
-@election_admin_required
-def admin_new_election():
- form = forms.ElectionForm()
- if form.validate_on_submit():
- election = models.Election(**remove_csrf(form.data))
- # Fix start_date and end_date to use datetime
- election.start_date = datetime.combine(election.start_date, time())
- election.end_date = datetime.combine(election.end_date,
- time(23, 59, 59))
- election.fas_user = flask.g.fas_user.username
- db.session.add(election)
- admin = models.ElectionAdminGroup(election=election,
- group_name=app.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
- role_required=u'user')
- db.session.add(admin)
- db.session.commit()
- flask.flash('Election "%s" added' % election.summary)
- fedmsgshim.publish(topic="election.new", msg=election)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/election_form.html', form=form,
- submit_text='Create election')
-
-
-(a)app.route('/admin/<election_alias>/')
-@election_admin_required
-def admin_view_election(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
- if not election:
- flask.abort(404)
- return flask.render_template('admin/view_election.html', election=election)
-
-
-(a)app.route('/admin/<election_alias>/edit', methods=("GET", "POST"))
-@election_admin_required
-def admin_edit_election(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
- if not election:
- flask.abort(404)
- form = forms.ElectionForm(election.id, obj=election)
- if form.validate_on_submit():
- form.populate_obj(election)
- db.session.commit()
- flask.flash('Election "%s" saved' % election.summary)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/election_form.html', form=form,
- submit_text='Edit election')
-
-
-(a)app.route('/admin/<election_alias>/candidates/new', methods=("GET", "POST"))
-@election_admin_required
-def admin_add_candidate(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
- if not election:
- flask.abort(404)
- form = forms.CandidateForm()
- if form.validate_on_submit():
- candidate = models.Candidate(election=election,
- **remove_csrf(form.data))
- db.session.add(candidate)
- db.session.commit()
- flask.flash('Candidate "%s" saved' % candidate.name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/candidate.html', form=form,
- submit_text='Add candidate')
-
-
-(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
- methods=("GET", "POST"))
-@election_admin_required
-def admin_edit_candidate(election_alias, candidate_id):
- election = models.Election.query.filter_by(alias=election_alias).first()
- candidate = models.Candidate.query.filter_by(id=candidate_id).first()
- if not election or not candidate:
- flask.abort(404)
- if candidate.election != election:
- flask.abort(404)
- form = forms.CandidateForm(obj=candidate)
- if form.validate_on_submit():
- form.populate_obj(candidate)
- db.session.commit()
- flask.flash('Candidate "%s" saved' % candidate.name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/candidate.html', form=form,
- submit_text='Edit candidate')
-
-
-(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
- methods=("GET", "POST"))
-@election_admin_required
-def admin_delete_candidate(election_alias, candidate_id):
- election = models.Election.query.filter_by(alias=election_alias).first()
- candidate = models.Candidate.query.filter_by(id=candidate_id).first()
- if not election or not candidate:
- flask.abort(404)
- if candidate.election != election:
- flask.abort(404)
- form = forms.ConfirmationForm()
- if form.validate_on_submit():
- db.session.delete(candidate)
- candidate_name = candidate.name
- db.session.commit()
- flask.flash('Candidate "%s" deleted' % candidate_name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/delete_candidate.html', form=form,
- candidate=candidate)
-
-
-(a)app.route('/results/<election_alias>')
-def election_results(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
-
- if election.status == 'In progress':
- flask.flash("We are sorry. The results for this election cannot be" \
- " viewed at this time because the election is still in" \
- " progress.")
- return redirect.safe_redirect_back()
-
- elif election.status == 'Pending':
- flask.flash("We are sorry. The results for this election cannot be" \
- " viewed at this time because the election has not" \
- " started.")
- return redirect.safe_redirect_back()
-
- elif election.embargoed == 1:
- if flask.g.fas_user is None:
- flask.flash("We are sorry. The results for this election" \
- "cannot be viewed because they are currently" \
- " embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
- else:
- if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
- flask.g.fas_user.groups:
- pass
- else:
- match = 0
- admingroups = models.ElectionAdminGroup.query.filter_by(
- election_id=election.id).all()
- for admingroup in admingroups:
- if admingroup.group_name in flask.g.fas_user.groups:
- match = 1
-
- if match == 0:
- flask.flash("We are sorry. The results for this election" \
- "cannot be viewed because they are currently" \
- " embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template('election/results.html', election=election,
- usernamemap=usernamemap)
diff --git a/fedora-elections/createdb.in b/fedora-elections/createdb.in
deleted file mode 100644
index c7a87da..0000000
--- a/fedora-elections/createdb.in
+++ /dev/null
@@ -1 +0,0 @@
-FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora-elections/fedora-elections.cfg python @DATADIR(a)/fedora-elections/createdb.py
diff --git a/fedora-elections/createdb.py b/fedora-elections/createdb.py
deleted file mode 100644
index a909c5e..0000000
--- a/fedora-elections/createdb.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7']
-import pkg_resources
-
-from fedora_elections import db
-db.create_all()
diff --git a/fedora-elections/default_config.py b/fedora-elections/default_config.py
deleted file mode 100644
index 7b5b794..0000000
--- a/fedora-elections/default_config.py
+++ /dev/null
@@ -1 +0,0 @@
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
diff --git a/fedora-elections/fedmsgshim.py b/fedora-elections/fedmsgshim.py
deleted file mode 100644
index 775f785..0000000
--- a/fedora-elections/fedmsgshim.py
+++ /dev/null
@@ -1,17 +0,0 @@
-""" This is a temporary shim that allows fedmsg to be an optional dependency of
-elections. If fedmsg is installed, these function calls will try to actually send
-messages. If it is not installed, it will return silently.
-
- :Author: Ralph Bean <rbean(a)redhat.com>
-
-"""
-
-import warnings
-
-
-def publish(*args, **kwargs):
- try:
- import fedmsg
- fedmsg.publish(*args, **kwargs)
- except Exception, e:
- warnings.warn(str(e))
diff --git a/fedora-elections/fedora-elections.cfg.in b/fedora-elections/fedora-elections.cfg.in
deleted file mode 100644
index ae68f66..0000000
--- a/fedora-elections/fedora-elections.cfg.in
+++ /dev/null
@@ -1,18 +0,0 @@
-import os, os.path
-
-DEBUG = True
-
-
-# You will want to change this for your install
-SECRET_KEY = 'change me'
-SQLALCHEMY_DATABASE_URI = 'sqlite:///@SHAREDSTDIR@/fedora-elections/database.sqlite'
-
-FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
-FAS_USERNAME = 'fcelections'
-FAS_PASSWORD = 'el3cTo$hozer'
-FAS_CHECK_CERT = False
-FAS_HTTPS_REQUIRED = False
-
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
-# List of URLs for additional site-wide CSS
-EXTRA_CSS = []
diff --git a/fedora-elections/fedora-elections.wsgi.in b/fedora-elections/fedora-elections.wsgi.in
deleted file mode 100644
index 430cc5c..0000000
--- a/fedora-elections/fedora-elections.wsgi.in
+++ /dev/null
@@ -1,21 +0,0 @@
-#-*- coding: UTF-8 -*-
-
-# These two lines are needed to run on EL6
-import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
-import pkg_resources
-
-import os
-import sys
-
-sys.stdout = sys.stderr
-
-## Set the environment variable pointing to the configuration file
-os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
-
-## The following is only needed if you did not install
-## as a python module (for example if you run it from a git clone).
-import sys
-# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
-
-from fedora_elections import app as application
diff --git a/fedora-elections/forms.py b/fedora-elections/forms.py
deleted file mode 100644
index f050dbd..0000000
--- a/fedora-elections/forms.py
+++ /dev/null
@@ -1,69 +0,0 @@
-from flask.ext import wtf
-
-from fedora_elections.models import Election
-
-
-class ElectionForm(wtf.Form):
- summary = wtf.TextField('Summary', [wtf.validators.Required(),
- wtf.validators.Length(max=150)])
- alias = wtf.TextField('Alias', [
- wtf.validators.Required(),
- wtf.validators.Length(max=100),
- wtf.validators.Regexp('[a-z0-9_-]+',
- message=('Alias may only contain lower case '
- 'letters, numbers, hyphens and '
- 'underscores.')),
- ])
- description = wtf.TextAreaField('Description', [wtf.validators.Required()])
- url = wtf.TextField('URL', [wtf.validators.Required(),
- wtf.validators.URL(),
- wtf.validators.Length(max=250)])
- start_date = wtf.DateField('Start date', [wtf.validators.Required()])
- end_date = wtf.DateField('End date', [wtf.validators.Required()])
- number_elected = wtf.IntegerField('Number elected',
- [wtf.validators.Required(),
- wtf.validators.NumberRange(min=1)],
- default=1)
- candidates_are_fasusers = wtf.BooleanField('Candidates are FAS users?')
- frontpage = wtf.BooleanField('Show on front page?')
- embargoed = wtf.BooleanField('Embargo results?', default=True)
-
- def __init__(form, election_id=None, *args, **kwargs):
- super(ElectionForm, form).__init__(*args, **kwargs)
- form._election_id = election_id
-
- def validate_summary(form, field):
- check = Election.query.filter_by(summary=form.summary.data).all()
- if check:
- if not (form._election_id and form._election_id == check[0].id):
- raise wtf.ValidationError('There is already another election '
- 'with this summary.')
-
- def validate_alias(form, field):
- if form.alias.data == 'new':
- raise wtf.ValidationError(flask.Markup('The alias cannot be '
- '<code>new</code>.'))
- check = Election.query.filter_by(alias=form.alias.data).all()
- if check:
- if not (form._election_id and form._election_id == check[0].id):
- raise wtf.ValidationError('There is already another election '
- 'with this alias.')
-
- def validate_end_date(form, field):
- if form.end_date.data <= form.start_date.data:
- raise wtf.ValidationError('End date must be later than '
- 'start date.')
-
-
-class CandidateForm(wtf.Form):
- name = wtf.TextField('Name', [wtf.validators.Required()])
- url = wtf.TextField('URL', [wtf.validators.Length(max=250)])
-
-
-class LoginForm(wtf.Form):
- username = wtf.TextField('Username', [wtf.validators.Required()])
- password = wtf.PasswordField('Password', [wtf.validators.Required()])
-
-
-class ConfirmationForm(wtf.Form):
- pass
diff --git a/fedora-elections/httpd-fedora-elections.conf.in b/fedora-elections/httpd-fedora-elections.conf.in
deleted file mode 100644
index c974b88..0000000
--- a/fedora-elections/httpd-fedora-elections.conf.in
+++ /dev/null
@@ -1,23 +0,0 @@
-Alias /fedora-elections/images @DATADIR@/fedora-elections/images
-Alias /fedora-elections/static @DATADIR@/fedora-elections/static
-
-WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
-WSGISocketPrefix run/wsgi
-WSGIRestrictStdout On
-WSGIRestrictSignal Off
-WSGIPythonOptimize 1
-
-WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora-elections/fedora-elections.wsgi
-
-<Location /fedora-elections>
- WSGIProcessGroup fedora-elections
- <IfModule mod_authz_core.c>
- # Apache 2.4
- Require all granted
- </IfModule>
- <IfModule !mod_authz_core.c>
- # Apache 2.2
- Order deny,allow
- Allow from all
- </IfModule>
-</Location>
diff --git a/fedora-elections/images/border-left.png b/fedora-elections/images/border-left.png
deleted file mode 100644
index 6f256df..0000000
Binary files a/fedora-elections/images/border-left.png and /dev/null differ
diff --git a/fedora-elections/images/border-right.png b/fedora-elections/images/border-right.png
deleted file mode 100644
index 0e7365b..0000000
Binary files a/fedora-elections/images/border-right.png and /dev/null differ
diff --git a/fedora-elections/images/fedora-logo.png b/fedora-elections/images/fedora-logo.png
deleted file mode 100644
index ce95893..0000000
Binary files a/fedora-elections/images/fedora-logo.png and /dev/null differ
diff --git a/fedora-elections/images/line.png b/fedora-elections/images/line.png
deleted file mode 100644
index 3498cb9..0000000
Binary files a/fedora-elections/images/line.png and /dev/null differ
diff --git a/fedora-elections/models.py b/fedora-elections/models.py
deleted file mode 100644
index b19bd33..0000000
--- a/fedora-elections/models.py
+++ /dev/null
@@ -1,103 +0,0 @@
-from fedora_elections import app, db
-
-from datetime import datetime
-
-class Election(db.Model):
- __tablename__ = 'elections'
-
- id = db.Column(db.Integer, primary_key=True)
- summary = db.Column(db.Unicode(150), unique=True, nullable=False)
- alias = db.Column(db.Unicode(100), unique=True, nullable=False)
- description = db.Column(db.UnicodeText, nullable=False)
- url = db.Column(db.Unicode(250), nullable=False)
- start_date = db.Column(db.DateTime, nullable=False)
- end_date = db.Column(db.DateTime, nullable=False)
- number_elected = db.Column(db.Integer, nullable=False, default=1)
- embargoed = db.Column(db.Boolean, nullable=False, default=True)
- frontpage = db.Column(db.Boolean, nullable=False, default=False)
- voting_type = db.Column(db.Unicode(100), nullable=False, default=u'range')
- candidates_are_fasusers = db.Column(db.Boolean, nullable=False,
- default=False)
- fas_user = db.Column(db.Unicode(50), nullable=False)
-
- @property
- def status(self):
- if datetime.now() < self.start_date:
- return 'Pending'
- else:
- if datetime.now() < self.end_date:
- return 'In progress'
- else:
- if self.embargoed:
- return 'Embargoed'
- else:
- return 'Ended'
-
- @property
- def locked(self):
- return datetime.now() >= self.start_date
-
-
-class ElectionAdminGroup(db.Model):
- __tablename__ = 'electionadmingroups'
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('admin_groups',
- lazy='dynamic'))
- group_name = db.Column(db.Unicode(150), nullable=False)
- role_required = db.Column(db.Enum(u'user', u'sponsor', u'administrator'),
- nullable=False)
-
-
-class Candidate(db.Model):
- __tablename__ = 'candidates'
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('candidates',
- lazy='dynamic'))
- name = db.Column(db.Unicode(150), nullable=False) # FAS username if
- # candidates_are_fasusers
- url = db.Column(db.Unicode(250))
-
- @property
- def vote_count(self):
- values = [v.value for v in self.votes]
- return sum(values)
-
-
-class LegalVoter(db.Model):
- __tablename__ = 'legalvoters'
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('legal_voters',
- lazy='dynamic'))
- # special names:
- # "cla + one" = cla_done + 1 non-cla group
- group_name = db.Column(db.Unicode(150), nullable=False)
-
-
-class Vote(db.Model):
- __tablename__ = 'votes'
-
- __table_args__ = (db.UniqueConstraint('election_id', 'voter',
- 'candidate_id',
- name='eid_voter_cid'), {} )
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('votes',
- lazy='dynamic'))
- voter = db.Column(db.Unicode(150), nullable=False)
- timestamp = db.Column(db.DateTime, nullable=False)
- candidate_id = db.Column(db.Integer, db.ForeignKey('candidates.id'),
- nullable=False)
- candidate = db.relationship('Candidate', backref=db.backref('votes',
- lazy='dynamic'))
- value = db.Column(db.Integer, nullable=False)
diff --git a/fedora-elections/redirect.py b/fedora-elections/redirect.py
deleted file mode 100644
index 5571abe..0000000
--- a/fedora-elections/redirect.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import flask
-
-from urlparse import urljoin, urlparse
-
-
-def is_safe_url(target):
- ref_url = urlparse(flask.request.host_url)
- test_url = urlparse(urljoin(flask.request.host_url, target))
- return test_url.scheme in ('http', 'https') and \
- ref_url.netloc == test_url.netloc
-
-
-def safe_redirect_back(next=None, fallback=('index', {})):
- targets = []
- if next:
- targets.append(next)
- if 'next' in flask.request.args and \
- flask.request.args['next']:
- targets.append(flask.request.args['next'])
- targets.append(flask.url_for(fallback[0], **fallback[1]))
- for target in targets:
- if is_safe_url(target):
- return flask.redirect(target)
diff --git a/fedora-elections/static/style.css b/fedora-elections/static/style.css
deleted file mode 100644
index ee09d79..0000000
--- a/fedora-elections/static/style.css
+++ /dev/null
@@ -1,144 +0,0 @@
-body {
- font-family: sans-serif;
- font-size: 12pt;
- line-height: 1.35;
-}
-
-#flashes {
- background-color: #ffa;
-}
-
-nav {
- background-color: #eee;
- padding: 0.5em 1em;
-}
-
-nav h2 {
- display: inline;
- font-size: inherit;
- margin-right: 1em;
-}
-
-nav ul {
- display: inline;
- list-style: none;
- padding: 0;
-}
-
-nav li {
- display: inline;
- margin-right: .5em;
-}
-
-.clear {
- clear: both;
-}
-
-/* input elements */
-
-fieldset {
- border: 0;
- padding: 0;
- margin: .5em 0;
- float: left;
- clear: both;
-}
-
-.row {
- margin: .25em 0;
-}
-
-.row label:first-child {
- width: 8em;
- margin-right: 0.75em;
- text-align: right;
- clear: both;
- font-weight: bold;
- display: inline-block;
- vertical-align: top;
-}
-
-.row textarea {
- width: 30em;
- height: 8em;
-}
-
-form .row .errors {
- list-style: none;
- margin: .5em 0 1em 10.9375em;
- padding: 0;
- font-size: .8em;
- color: #c00;
- width: 30em;
-}
-
-div.help {
- width: 40%;
- float: right;
- background-color: #ffa;
- border-top: 1px solid #ee9;
- padding: 1em;
-/* font-size: .8em; */
- margin: .625em 0;
-}
-
-div > *:first-child { margin-top: 0; }
-div > *:last-child { margin-bottom: 0; }
-
-form .buttons.indent {
- margin-left: 8.75em;
-}
-
-.vthot
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #FFFFFF;
- background: #9C0000;
-}
-
-.vtwarm
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #FFA300;
-}
-
-.vtmedwarm
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #93FF63;
-}
-
-.vtmedcool
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #00DAFE;
-}
-
-.vtcool
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #0033FF;
-}
-
-.vtcold
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #FFFFFF;
- background: #000083;
-}
diff --git a/fedora-elections/templates/_formhelpers.html b/fedora-elections/templates/_formhelpers.html
deleted file mode 100644
index ff781b7..0000000
--- a/fedora-elections/templates/_formhelpers.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% macro render_field(field, after="") %}
-<div class="row">
- {{ field.label }}{{ field(**kwargs)|safe }}{% if after %} {{ after }}{% endif %}
- {% if field.errors %}
- <ul class="errors">
- {% for error in field.errors %}
- <li>{{ error }}</li>
- {% endfor %}
- </ul>
- {% endif %}
-</div>
-{% endmacro %}
-{# vim: set ts=2 sw=2 sta et: #}
diff --git a/fedora-elections/templates/admin/all_elections.html b/fedora-elections/templates/admin/all_elections.html
deleted file mode 100644
index e168ac0..0000000
--- a/fedora-elections/templates/admin/all_elections.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% extends "base.html" %}
-{% block title %}Election administration{% endblock %}
-{% block body %}
- {% if num_elections > 0 %}
- <h2>Election administration</h2>
- <table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>
- <a href="{{ url_for('admin_view_election',
- election_alias=election.alias) }}">
- {{ election.summary }}
- </a>
- </td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- </tr>
- {% endfor %}
- </table>
- {% endif %}
-{%endblock %}
diff --git a/fedora-elections/templates/admin/candidate.html b/fedora-elections/templates/admin/candidate.html
deleted file mode 100644
index a7b2fda..0000000
--- a/fedora-elections/templates/admin/candidate.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}{{ submit_text }}{% endblock %}
-{% block body %}
- <h2>{{ submit_text }}</h2>
-
- <form action="" method="post">
- <fieldset>
- {{ render_field(form.name) }}
- {{ render_field(form.url) }}
- </fieldset>
-
- <br class="clear">
- <p class="buttons">
- <input type="submit" class="submit positive" value="{{ submit_text }}">
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora-elections/templates/admin/delete_candidate.html b/fedora-elections/templates/admin/delete_candidate.html
deleted file mode 100644
index d764f19..0000000
--- a/fedora-elections/templates/admin/delete_candidate.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}Delete candidate{% endblock %}
-{% block body %}
- <h2>{{ self.title() }}</h2>
-
- <form action="" method="post">
- <p>Are you sure you want to delete candidate "{{ candidate.name }}"?</p>
- <p class="buttons">
- <input type="submit" value="Delete candidate">
- <a href="{{ url_for('admin_view_election',
- election_alias=candidate.election.alias) }}">
- Cancel
- </a>
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora-elections/templates/admin/election_form.html b/fedora-elections/templates/admin/election_form.html
deleted file mode 100644
index 6e2a3d6..0000000
--- a/fedora-elections/templates/admin/election_form.html
+++ /dev/null
@@ -1,77 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}{{ submit_text }}{% endblock %}
-{% block body %}
- <h2>{{ self.title() }}</h2>
-
- <form action="" method="post">
- <fieldset>
- {{ render_field(form.summary) }}
- </fieldset>
- <div class="help">
- <p>This is the name of the election that users will first see.</p>
- <p>Example: <kbd>Fedora Board, November 2010</kbd></p>
- </div>
- <fieldset>
- {{ render_field(form.alias) }}
- </fieldset>
- <div class="help">
- <p>
- A short, unique name that will be used for the election's URL.
- Lowercase letters, numbers, hyphens and underscores only.
- </p>
- <p>Example: <kbd>boardf15</kbd></p>
- </div>
- <fieldset>
- {{ render_field(form.description) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.url) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.start_date, " at 00:00:00 UTC") }}
- {{ render_field(form.end_date, " at 23:59:59 UTC") }}
- </fieldset>
- <div class="help">
- <p>Enter dates in <kbd>YYYY-MM-DD</kbd> format.</p>
- </div>
- <fieldset>
- {{ render_field(form.number_elected) }}
- </fieldset>
- <div class="help">
- <p>
- This determines the cutoff for how many candidates win the election.
- For example, if you are electing four empty seats on a committee,
- enter <kbd>4</kbd> here.
- </p>
- </div>
- <fieldset>
- {{ render_field(form.candidates_are_fasusers) }}
- </fieldset>
- <div class="help">
- <p>
- Check this if every candidate in this election has a FAS user account.
- This will automatically pull full names and wiki page URLs from FAS.
- </p>
- </div>
- <fieldset>
- {{ render_field(form.frontpage) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.embargoed) }}
- </fieldset>
- <div class="help">
- <p>
- Embargo results when the election is finished. To unembargo them, edit
- the election and uncheck this box.
- </p>
- </div>
-
- <br class="clear">
- {% block election_form_extra %}{% endblock %}
- <p class="buttons">
- <input type="submit" class="submit positive" value="Save Changes">
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora-elections/templates/admin/view_election.html b/fedora-elections/templates/admin/view_election.html
deleted file mode 100644
index ed070f3..0000000
--- a/fedora-elections/templates/admin/view_election.html
+++ /dev/null
@@ -1,42 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- <h2>{{ election.summary }}</h2>
- <p>{{ election.description }}</p>
- <p>
- <a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">
- Edit election
- </a>
- </p>
- <ul>
- <li>On front page: {{ election.frontpage }}</li>
- <li>Start date: {{ election.start_date }}</li>
- <li>End date: {{ election.end_date }}</li>
- <li>Number elected: {{ election.number_elected }}</li>
- <li>Election type: {{ election.voting_type }}</li>
- </ul>
- <h3>Candidates</h3>
- {% if election.candidates.all() %}
- <ul>
- {% for candidate in election.candidates.all() %}
- <li>{{ candidate.name }}
- <a href="{{ url_for('admin_edit_candidate',
- election_alias=election.alias,
- candidate_id=candidate.id) }}">
- [edit]
- </a>
- <a href="{{ url_for('admin_delete_candidate',
- election_alias=election.alias,
- candidate_id=candidate.id) }}">
- [x]
- </a>
- </li>
- {% endfor %}
- </ul>
- {% else %}
- <p>There are no candidates.</p>
- {% endif %}
- <a href="{{ url_for('admin_add_candidate',
- election_alias=election.alias) }}">
- Add a candidate
- </a>
-{% endblock %}
diff --git a/fedora-elections/templates/auth/login.html b/fedora-elections/templates/auth/login.html
deleted file mode 100644
index 9e7b73c..0000000
--- a/fedora-elections/templates/auth/login.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}Log in{% endblock %}
-{% block body %}
- <h2>Log in</h2>
-
- <form action="" method="post">
- <p>
- Log in using your
- <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a>
- account.
- </p>
- {{ render_field(form.username) }}
- {{ render_field(form.password) }}
- <p class="buttons indent"><input type="submit" class="submit positive"
- value="Log in">
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora-elections/templates/base.html b/fedora-elections/templates/base.html
deleted file mode 100644
index 3e6f806..0000000
--- a/fedora-elections/templates/base.html
+++ /dev/null
@@ -1,134 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- {% block head %}
- <title>
- Fedora Elections
- {% if self.title() %}
- —
- {% block title %}{% endblock %}
- {% endif %}
- </title>
- <link rel="stylesheet" type="text/css" media="screen"
- href="{{ url_for("static", filename="style.css") }}" />
- <link rel="stylesheet" type="text/css" media="screen"
- href="https://fedoraproject.org/static/css/fedora.css" />
- {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
- {% endfor %}
- {% endblock %}
- </head>
- <body>
- <div id="wrapper">
- <div id="head">
- <h1><a href="https://admin.fedoraproject.org/en/index">Fedora</a></h1>
- </div>
-
- <div id="sidebar">
- <div id="nav">
- <h2>Navigation</h2>
- <ul>
- <li id="nav-home">
- <a href="http://fedoraproject.org/en/index">Home</a>
- </li>
- <li id="nav-get">
- <a href="http://fedoraproject.org/en/get-fedora">Get Fedora</a>
- </li>
- <li id="nav-join">
- <a href="http://fedoraproject.org/en/join-fedora">Join Fedora</a>
- </li>
- <li id="nav-help">
- <a href="http://fedoraproject.org/en/get-help">Get Help</a>
- </li>
- </ul>
- </div>
- </div>
-
- <div id="content">
- <ul class="toolbar" id="#main-toolbar">
- {% if g.fas_user %}
- <li>
- Hello, {{ g.fas_user.username }} —
- <a href="{{ url_for("auth_logout") }}">log out</a>
- </li>
- {% else %}
- <li>
- You are not logged in —
- <a href="{{ url_for("auth_login") }}">log in</a>
- </li>
- {% endif %}
- </ul>
- <h2>Elections</h2>
- <ul>
- <li>
- <a href="{{ url_for("open_elections") }}">
- Open elections
- </a>
- </li>
- <li>
- <a href="{{ url_for("archived_elections") }}">
- Archived elections
- </a>
- </li>
- </ul>
- <br>
- <h2>Administration</h2>
- <ul>
- <li>
- <a href="{{ url_for("admin_view_elections") }}">
- My elections
- </a>
- </li>
- <li>
- <a href="{{ url_for("admin_new_election") }}">
- Create new election
- </a>
- </li>
- </ul>
- {% with messages = get_flashed_messages() %}
- {% if messages %}
- <ul id="flashes">
- {% for message in messages %}
- <li>{{ message }}</li>
- {% endfor %}
- </ul>
- {% endif %}
- {% endwith %}
- {% block body %}
- {% endblock %}
- </div>
- </div>
- <div id="bottom">
- <div id="footer">
- {% block footer %}
- <p class="copy">
- Copyright © 2008 Red Hat, Inc. and others. Please send any
- comments or corrections to the
- <a href="mailto:webmaster@fedoraproject.org">
- websites team
- </a>.
- </p>
- <p class="disclaimer">
- The Fedora Project is maintained and driven by the community and
- sponsored by Red Hat. This is a community maintained site. Red
- Hat is not responsible for content.
- </p>
- <ul>
- <li class="first">
- <a href="/en/sponsors">Sponsors</a>
- </li>
- <li>
- <a href="http://fedoraproject.org/wiki/Legal">
- Legal
- </a>
- </li>
- <li>
- <a href="http://fedoraproject.org/wiki/Legal/TrademarkGuidelines">
- Trademark Guidelines
- </a>
- </li>
- </ul>
- </div>
- {% endblock %}
- </div>
- </body>
-</html>
diff --git a/fedora-elections/templates/election/about.html b/fedora-elections/templates/election/about.html
deleted file mode 100644
index c7bf0ec..0000000
--- a/fedora-elections/templates/election/about.html
+++ /dev/null
@@ -1,40 +0,0 @@
-{% extends "base.html" %}
-{% block title %}Election Information{% endblock %}
-{% block body %}
- <h1>{{ election.summary }}</h1>
- <p>{{ election.description }}</p>
- {% if election.url %}
- <p><a href="{{ election.url }}">[More Information]</a></p>
- {% endif %}
- {% if election.status == 'Ended' %}
- <a href="/results/{{ election.alias }}">Show Results!</a>
- {% endif %}
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- {% if candidate.url %}
- <small><a href="{{ candidate.url }}">[info]</a></small>
- {% endif %}
- </td>
- </tr>
- {% endfor %}
- </table>
- {% if election.legal_voters.count() != 0 %}
- <p>
- To vote in this election you must be a member of any one of the following groups:
- </p>
- <ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
- </ul>
- {% else %}
- <p />
- {% endif %}
-{% endblock %}
diff --git a/fedora-elections/templates/election/archive.html b/fedora-elections/templates/election/archive.html
deleted file mode 100644
index 8740b3c..0000000
--- a/fedora-elections/templates/election/archive.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- {% if num_elections > 0 %}
- <h2>Past Elections</h2>
- <table border="1" cellpadding="1">
- <tr>
- <th></th>
- <th>Start Date (UTC)</th>
- <th>End Date (UTC)</th>
- <th></th>
- <th></th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>{{election.description}}</td>
- <td>{{election.start_date}}</td>
- <td>{{election.end_date}}</td>
- <td>
- <a href="about/{{ election.alias }}"">More information</a>
- </td>
- <td>
- <a href="results/{{ election.alias }}">Results</a>
- </td>
- </tr>
- {% endfor %}
- </table>
- {% endif %}
-{% endblock %}
diff --git a/fedora-elections/templates/election/results.html b/fedora-elections/templates/election/results.html
deleted file mode 100644
index f61d214..0000000
--- a/fedora-elections/templates/election/results.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- <h1>{{ election.summary }} Results</h1>
- <p>{{ election.description }}</p>
- {% if election.url %}
- <p><a href="{{ election.url }}">[More Information]</a></p>
- {% endif %}
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{candidate.name}}
- {% endif %}
- {% if candidate.url %}
- <small><a href="{{ candidate.url }}">[info]</a></small>
- {% endif %}
- </td>
- <td>{{ candidate.vote_count }}</td>
- </tr>
- {% endfor %}
- </table>
-{% endblock %}
diff --git a/fedora-elections/templates/election/vote.html b/fedora-elections/templates/election/vote.html
deleted file mode 100644
index e3c4dd4..0000000
--- a/fedora-elections/templates/election/vote.html
+++ /dev/null
@@ -1,116 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-<h2>{{election.summary}}</h2>
-
-<p>{{election.description}}</p>
-
-<p><a href="{{election.url}}">[More Information]</a></p>
-
-<form action="" method="post">
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- <a href="{{candidate.url}}">[Info]</a>
- </td>
- <!-- If user is logged in: -->
- {% if g.fas_user %}
- {% if nextaction == 'vote' %}
- <td>
- <select name="{{candidate.id}}">
- {% for opt in range(0, num_candidates+1) %}
- <option value={{opt}}>{{opt}}</option>
- {% endfor %}
- </select>
- </td>
- {% endif %}
- {% if nextaction == 'confirm' %}
- <td>
- {% set cid = candidate.id %}
- <input type="hidden" name="{{candidate.id}}"
- value="{{voteinfo[cid]}}" />
- {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
- {% if weightpercent >= 90 %}
- <div class="vthot">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 70 %}
- <div class="vtwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 50 %}
- <div class="vtmedwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 30 %}
- <div class="vtmedcool">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 10 %}
- <div class="vtcool">
- {{voteinfo[cid]}}
- </div>
- {% else %}
- <div class="vtcold">
- {{voteinfo[cid]}}
- </div>
- {% endif %}
- </td>
- {% endif %}
- {% endif %}
- <!-- End if user logged in -->
- </tr>
- {% endfor %}
- </table>
- {% if g.fas_user %}
- <p>
- {% if nextaction == 'confirm' %}
- <input type="submit" name="{{nextaction}}" value="Submit" />
- {% else %}
- <input type="submit" name="{{nextaction}}" value="Preview" />
- {% endif %}
- </p>
- {% endif %}
-</form>
-
-<!-- Make this conditional on whether the user is logged in and can vote in the election -->
-{% if election.legal_voters.count() != 0 %}
-<p>
- To vote in this election you must be a member of any one of the following groups:
-</p>
-<ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
-</ul>
-{% else %}
-<p />
-{% endif %}
-<div>
- <h2>Information on Range Voting</h2>
- <p>
- Fedora Project has implemented
- <a href="http://rangevoting.org/">Range Voting</a>
- for this election, in particular the "Range (score-summing, blanks
- treated as zero score, no quorum rule)" range voting system.
- </p>
- <p>
- To cast your vote in this election simply select a value between 0 and
- {{num_candidates}} with 0 as 'least or no preference' and
- {{num_candidates}} as 'highest preference'.
- </p>
- <p>
- At the end of the election, the highest ranking candidate(s) are marked
- as the winners.
- </p>
- <p>
- For more information about Range Voting, visit the
- <a href="http://rangevoting.org/">Center for Range Voting</a>.
- </p>
-</div>
-{% endblock %}
diff --git a/fedora-elections/templates/list/index.html b/fedora-elections/templates/list/index.html
deleted file mode 100644
index 290c2cf..0000000
--- a/fedora-elections/templates/list/index.html
+++ /dev/null
@@ -1,32 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- {% if num_elections > 0 %}
- <h2>{{ title }}</h2>
- <table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- <th></th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>{{ election.summary }}</td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- <td>
- {% if election.status == "In progress" %}
- {% if g.fas_user %}
- <a href="{{ url_for('vote', election_alias=election.alias) }}">
- Vote now!
- </a>
- {% endif %}
- {% endif %}
- </td>
- </tr>
- {% endfor %}
- </table>
- {% endif %}
-{% endblock %}
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
new file mode 100644
index 0000000..86bfdc6
--- /dev/null
+++ b/fedora_elections/__init__.py
@@ -0,0 +1,481 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2012 Red Hat, Inc.
+# Copyright © 2012 Ian Weller <ianweller(a)fedoraproject.org>
+# Copyright © 2012 Toshio Kuratomi <tkuratom(a)redhat.com>
+# Copyright © 2012 Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+#
+# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
+# Toshio Kuratomi <tkuratom(a)redhat.com>
+# Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+
+import flask
+from flask.ext.fas import FAS
+from flask.ext.sqlalchemy import SQLAlchemy
+
+from fedora.client import AuthError, AppError
+from fedora.client.fas2 import AccountSystem
+
+from sqlalchemy.orm.exc import NoResultFound
+
+import fedmsgshim
+
+from datetime import datetime, time
+from functools import wraps
+from urlparse import urlparse, urljoin
+
+app = flask.Flask(__name__)
+app.config.from_object('fedora_elections.default_config')
+app.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
+
+# set up SQLAlchemy
+db = SQLAlchemy(app)
+
+# set up FAS
+fas = FAS(app)
+
+# FAS for usernames.
+# baseURL = "https://admin.fedoraproject.org/accounts"
+# fasuser = "elections"
+# faspw = "elections"
+# fas2_url = 'http://fakefas.fedoraproject.org/accounts/'
+fas2_url = app.config['FAS_BASE_URL']
+fas2_username = app.config['FAS_USERNAME']
+fas2_password = app.config['FAS_PASSWORD']
+fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password,
+ insecure=True)
+
+
+# modular imports
+from fedora_elections import forms, models, redirect
+
+
+def remove_csrf(form_data):
+ return dict([(k, v) for k, v in form_data.items() if k != 'csrf'])
+
+
+def login_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if flask.g.fas_user is None:
+ return flask.redirect(flask.url_for('auth_login',
+ next=flask.request.url))
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+def election_admin_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if flask.g.fas_user is None:
+ return flask.redirect(flask.url_for('auth_login',
+ next=flask.request.url))
+ if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
+ flask.g.fas_user.groups:
+ flask.abort(403)
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+### LIST VIEWS #############################################
+
+(a)app.route('/')
+def index():
+ elections = models.Election.query.filter_by(frontpage=True).all()
+ num_elections = len(elections)
+ return flask.render_template('list/index.html', elections=elections,
+ num_elections=num_elections,
+ title="Elections")
+
+
+(a)app.route('/about/<election_alias>')
+def about_election(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/about.html', election=election,
+ usernamemap=usernamemap)
+
+
+(a)app.route('/archive')
+def archived_elections():
+ now = datetime.utcnow()
+
+ try:
+ elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date<now, models.Election.id>0).all()
+ except NoResultFound:
+ flask.flash('There are no archived elections.')
+ return redirect.safe_redirect_back()
+
+ num_elections = len(elections)
+ if (num_elections == 0):
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('election/archive.html',
+ num_elections=num_elections,
+ elections=elections)
+
+
+(a)app.route('/open')
+def open_elections():
+ now = datetime.utcnow()
+
+ try:
+ elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date>now,
+ models.Election.id>0).all()
+ except NoResultFound:
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ num_elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date>now,
+ models.Election.id>0).count()
+ if (num_elections == 0):
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('list/index.html', elections=elections,
+ num_elections=num_elections,
+ title='Open Elections')
+
+
+### ELECTION VIEWS #############################################
+
+(a)app.route('/vote/<election_alias>', methods=['GET', 'POST'])
+def vote(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'Pending':
+ flask.flash('Voting has not yet started, sorry.')
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Ended':
+ flask.flash('This election is closed. You have been redirected to ' +
+ 'the election results.')
+ return flask.redirect(flask.url_for('results', election.shortname))
+
+ votes = models.Vote.query.filter_by(election_id=election.id,
+ voter=flask.g.fas_user.username).count()
+ if (votes != 0):
+ flask.flash('You have already voted in the election!')
+ return redirect.safe_redirect_back()
+
+ num_candidates = election.candidates.count()
+
+ uvotes = {}
+ for candidate in election.candidates:
+ uvotes[candidate.id] = ""
+ next_action = 'vote'
+
+ if flask.request.method == 'POST':
+ next_action = ''
+ form_values = flask.request.form.values()
+ if 'Submit' in form_values:
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote >= 0 and vote <= num_candidates:
+ uvotes[candidate.id] = int(vote)
+ else:
+ flask.flash("Invalid Ballot!")
+ return redirect.safe_redirect_back()
+
+ except ValueError:
+ flask.flash("Invalid Ballot!")
+ return redirect.safe_redirect_back()
+
+ for uvote in uvotes:
+ new_vote = models.Vote(election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=uvote,
+ value=uvotes[uvote] )
+ db.session.add(new_vote)
+ db.session.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return redirect.safe_redirect_back()
+
+ elif 'Preview' in form_values:
+ flask.flash("Please confirm your vote!")
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote > num_candidates:
+ flask.flash("Invalid data.")
+ uvotes[candidate.id] = 0
+ next_action = 'vote'
+ elif vote >= 0:
+ uvotes[candidate.id] = vote
+ else:
+ flask.flash("Invalid data2.")
+ uvotes[c.id] = 0
+ next_action = 'vote'
+ except ValueError:
+ flask.flash("Invalid data")
+
+ if (next_action != 'vote'):
+ next_action = 'confirm'
+
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/vote.html', election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ voteinfo=uvotes, nextaction=next_action)
+
+
+### AUTH VIEWS #############################################
+
+(a)app.route('/login', methods=('GET', 'POST'))
+def auth_login():
+ if flask.g.fas_user:
+ return redirect.safe_redirect_back()
+ form = forms.LoginForm()
+ if form.validate_on_submit():
+ if fas.login(form.username.data, form.password.data):
+ flask.flash('Welcome, %s' % flask.g.fas_user.username)
+ return redirect.safe_redirect_back()
+ else:
+ flask.flash('Incorrect username or password')
+ return flask.render_template('auth/login.html', form=form)
+
+
+(a)app.route('/logout')
+def auth_logout():
+ if not flask.g.fas_user:
+ return redirect.safe_redirect_back()
+ fas.logout()
+ flask.flash('You have been logged out')
+ return redirect.safe_redirect_back()
+
+
+### ELECTION ADMIN VIEWS ###################################
+
+(a)app.route('/admin/')
+@election_admin_required
+def admin_view_elections():
+ elections = models.Election.query.\
+ filter_by(fas_user=flask.g.fas_user.username).all()
+ num_elections = len(elections)
+ if (num_elections == 0):
+ flask.flash('You do not have any elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('admin/all_elections.html',
+ num_elections=num_elections,
+ elections=elections)
+
+
+(a)app.route('/admin/new', methods=("GET", "POST"))
+@election_admin_required
+def admin_new_election():
+ form = forms.ElectionForm()
+ if form.validate_on_submit():
+ election = models.Election(**remove_csrf(form.data))
+ # Fix start_date and end_date to use datetime
+ election.start_date = datetime.combine(election.start_date, time())
+ election.end_date = datetime.combine(election.end_date,
+ time(23, 59, 59))
+ election.fas_user = flask.g.fas_user.username
+ db.session.add(election)
+ admin = models.ElectionAdminGroup(election=election,
+ group_name=app.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ role_required=u'user')
+ db.session.add(admin)
+ db.session.commit()
+ flask.flash('Election "%s" added' % election.summary)
+ fedmsgshim.publish(topic="election.new", msg=election)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/election_form.html', form=form,
+ submit_text='Create election')
+
+
+(a)app.route('/admin/<election_alias>/')
+@election_admin_required
+def admin_view_election(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ return flask.render_template('admin/view_election.html', election=election)
+
+
+(a)app.route('/admin/<election_alias>/edit', methods=("GET", "POST"))
+@election_admin_required
+def admin_edit_election(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ form = forms.ElectionForm(election.id, obj=election)
+ if form.validate_on_submit():
+ form.populate_obj(election)
+ db.session.commit()
+ flask.flash('Election "%s" saved' % election.summary)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/election_form.html', form=form,
+ submit_text='Edit election')
+
+
+(a)app.route('/admin/<election_alias>/candidates/new', methods=("GET", "POST"))
+@election_admin_required
+def admin_add_candidate(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ form = forms.CandidateForm()
+ if form.validate_on_submit():
+ candidate = models.Candidate(election=election,
+ **remove_csrf(form.data))
+ db.session.add(candidate)
+ db.session.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/candidate.html', form=form,
+ submit_text='Add candidate')
+
+
+(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
+ methods=("GET", "POST"))
+@election_admin_required
+def admin_edit_candidate(election_alias, candidate_id):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ if not election or not candidate:
+ flask.abort(404)
+ if candidate.election != election:
+ flask.abort(404)
+ form = forms.CandidateForm(obj=candidate)
+ if form.validate_on_submit():
+ form.populate_obj(candidate)
+ db.session.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/candidate.html', form=form,
+ submit_text='Edit candidate')
+
+
+(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
+ methods=("GET", "POST"))
+@election_admin_required
+def admin_delete_candidate(election_alias, candidate_id):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ if not election or not candidate:
+ flask.abort(404)
+ if candidate.election != election:
+ flask.abort(404)
+ form = forms.ConfirmationForm()
+ if form.validate_on_submit():
+ db.session.delete(candidate)
+ candidate_name = candidate.name
+ db.session.commit()
+ flask.flash('Candidate "%s" deleted' % candidate_name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/delete_candidate.html', form=form,
+ candidate=candidate)
+
+
+(a)app.route('/results/<election_alias>')
+def election_results(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'In progress':
+ flask.flash("We are sorry. The results for this election cannot be" \
+ " viewed at this time because the election is still in" \
+ " progress.")
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Pending':
+ flask.flash("We are sorry. The results for this election cannot be" \
+ " viewed at this time because the election has not" \
+ " started.")
+ return redirect.safe_redirect_back()
+
+ elif election.embargoed == 1:
+ if flask.g.fas_user is None:
+ flask.flash("We are sorry. The results for this election" \
+ "cannot be viewed because they are currently" \
+ " embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
+ else:
+ if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
+ flask.g.fas_user.groups:
+ pass
+ else:
+ match = 0
+ admingroups = models.ElectionAdminGroup.query.filter_by(
+ election_id=election.id).all()
+ for admingroup in admingroups:
+ if admingroup.group_name in flask.g.fas_user.groups:
+ match = 1
+
+ if match == 0:
+ flask.flash("We are sorry. The results for this election" \
+ "cannot be viewed because they are currently" \
+ " embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/results.html', election=election,
+ usernamemap=usernamemap)
diff --git a/fedora_elections/createdb.in b/fedora_elections/createdb.in
new file mode 100644
index 0000000..e921d8a
--- /dev/null
+++ b/fedora_elections/createdb.in
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora_elections/fedora-elections.cfg python @DATADIR(a)/fedora_elections/createdb.py
diff --git a/fedora_elections/createdb.py b/fedora_elections/createdb.py
new file mode 100644
index 0000000..a909c5e
--- /dev/null
+++ b/fedora_elections/createdb.py
@@ -0,0 +1,6 @@
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7']
+import pkg_resources
+
+from fedora_elections import db
+db.create_all()
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
new file mode 100644
index 0000000..7b5b794
--- /dev/null
+++ b/fedora_elections/default_config.py
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
diff --git a/fedora_elections/fedmsgshim.py b/fedora_elections/fedmsgshim.py
new file mode 100644
index 0000000..775f785
--- /dev/null
+++ b/fedora_elections/fedmsgshim.py
@@ -0,0 +1,17 @@
+""" This is a temporary shim that allows fedmsg to be an optional dependency of
+elections. If fedmsg is installed, these function calls will try to actually send
+messages. If it is not installed, it will return silently.
+
+ :Author: Ralph Bean <rbean(a)redhat.com>
+
+"""
+
+import warnings
+
+
+def publish(*args, **kwargs):
+ try:
+ import fedmsg
+ fedmsg.publish(*args, **kwargs)
+ except Exception, e:
+ warnings.warn(str(e))
diff --git a/fedora_elections/fedora-elections.cfg.in b/fedora_elections/fedora-elections.cfg.in
new file mode 100644
index 0000000..4b49118
--- /dev/null
+++ b/fedora_elections/fedora-elections.cfg.in
@@ -0,0 +1,18 @@
+import os, os.path
+
+DEBUG = True
+
+
+# You will want to change this for your install
+SECRET_KEY = 'change me'
+SQLALCHEMY_DATABASE_URI = 'sqlite:///@SHAREDSTDIR@/fedora_elections/database.sqlite'
+
+FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
+FAS_USERNAME = 'fcelections'
+FAS_PASSWORD = 'el3cTo$hozer'
+FAS_CHECK_CERT = False
+FAS_FLASK_COOKIE_REQUIRES_HTTPS = False
+
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+# List of URLs for additional site-wide CSS
+EXTRA_CSS = []
diff --git a/fedora_elections/fedora-elections.wsgi.in b/fedora_elections/fedora-elections.wsgi.in
new file mode 100644
index 0000000..ecdccc9
--- /dev/null
+++ b/fedora_elections/fedora-elections.wsgi.in
@@ -0,0 +1,21 @@
+#-*- coding: UTF-8 -*-
+
+# These two lines are needed to run on EL6
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import os
+import sys
+
+sys.stdout = sys.stderr
+
+## Set the environment variable pointing to the configuration file
+os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora_elections/fedora-elections.cfg'
+
+## The following is only needed if you did not install
+## as a python module (for example if you run it from a git clone).
+import sys
+# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
+
+from fedora_elections import app as application
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
new file mode 100644
index 0000000..0f769a2
--- /dev/null
+++ b/fedora_elections/forms.py
@@ -0,0 +1,73 @@
+from flask.ext import wtf
+
+from fedora_elections.models import Election
+
+
+class ElectionForm(wtf.Form):
+ summary = wtf.TextField('Summary', [wtf.validators.Required(),
+ wtf.validators.Length(max=150)])
+ alias = wtf.TextField('Alias', [
+ wtf.validators.Required(),
+ wtf.validators.Length(max=100),
+ wtf.validators.Regexp('[a-z0-9_-]+',
+ message=('Alias may only contain lower case '
+ 'letters, numbers, hyphens and '
+ 'underscores.')),
+ ])
+ description = wtf.TextAreaField('Description', [wtf.validators.Required()])
+ voting_type = wtf.RadioField('Type',
+ choices=[('range', 'Range Voting'),
+ ('simple', 'Simple Voting')],
+ default='range')
+ url = wtf.TextField('URL', [wtf.validators.Required(),
+ wtf.validators.URL(),
+ wtf.validators.Length(max=250)])
+ start_date = wtf.DateField('Start date', [wtf.validators.Required()])
+ end_date = wtf.DateField('End date', [wtf.validators.Required()])
+ number_elected = wtf.IntegerField('Number elected',
+ [wtf.validators.Required(),
+ wtf.validators.NumberRange(min=1)],
+ default=1)
+ candidates_are_fasusers = wtf.BooleanField('Candidates are FAS users?')
+ frontpage = wtf.BooleanField('Show on front page?')
+ embargoed = wtf.BooleanField('Embargo results?', default=True)
+
+ def __init__(form, election_id=None, *args, **kwargs):
+ super(ElectionForm, form).__init__(*args, **kwargs)
+ form._election_id = election_id
+
+ def validate_summary(form, field):
+ check = Election.query.filter_by(summary=form.summary.data).all()
+ if check:
+ if not (form._election_id and form._election_id == check[0].id):
+ raise wtf.ValidationError('There is already another election '
+ 'with this summary.')
+
+ def validate_alias(form, field):
+ if form.alias.data == 'new':
+ raise wtf.ValidationError(flask.Markup('The alias cannot be '
+ '<code>new</code>.'))
+ check = Election.query.filter_by(alias=form.alias.data).all()
+ if check:
+ if not (form._election_id and form._election_id == check[0].id):
+ raise wtf.ValidationError('There is already another election '
+ 'with this alias.')
+
+ def validate_end_date(form, field):
+ if form.end_date.data <= form.start_date.data:
+ raise wtf.ValidationError('End date must be later than '
+ 'start date.')
+
+
+class CandidateForm(wtf.Form):
+ name = wtf.TextField('Name', [wtf.validators.Required()])
+ url = wtf.TextField('URL', [wtf.validators.Length(max=250)])
+
+
+class LoginForm(wtf.Form):
+ username = wtf.TextField('Username', [wtf.validators.Required()])
+ password = wtf.PasswordField('Password', [wtf.validators.Required()])
+
+
+class ConfirmationForm(wtf.Form):
+ pass
diff --git a/fedora_elections/httpd-fedora-elections.conf.in b/fedora_elections/httpd-fedora-elections.conf.in
new file mode 100644
index 0000000..c79d2d9
--- /dev/null
+++ b/fedora_elections/httpd-fedora-elections.conf.in
@@ -0,0 +1,23 @@
+Alias /fedora-elections/images @DATADIR@/fedora_elections/images
+Alias /fedora-elections/static @DATADIR@/fedora_elections/static
+
+WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
+WSGISocketPrefix run/wsgi
+WSGIRestrictStdout On
+WSGIRestrictSignal Off
+WSGIPythonOptimize 1
+
+WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora_elections/fedora-elections.wsgi
+
+<Location /fedora-elections>
+ WSGIProcessGroup fedora-elections
+ <IfModule mod_authz_core.c>
+ # Apache 2.4
+ Require all granted
+ </IfModule>
+ <IfModule !mod_authz_core.c>
+ # Apache 2.2
+ Order deny,allow
+ Allow from all
+ </IfModule>
+</Location>
diff --git a/fedora_elections/images/border-left.png b/fedora_elections/images/border-left.png
new file mode 100644
index 0000000..6f256df
Binary files /dev/null and b/fedora_elections/images/border-left.png differ
diff --git a/fedora_elections/images/border-right.png b/fedora_elections/images/border-right.png
new file mode 100644
index 0000000..0e7365b
Binary files /dev/null and b/fedora_elections/images/border-right.png differ
diff --git a/fedora_elections/images/fedora-logo.png b/fedora_elections/images/fedora-logo.png
new file mode 100644
index 0000000..ce95893
Binary files /dev/null and b/fedora_elections/images/fedora-logo.png differ
diff --git a/fedora_elections/images/line.png b/fedora_elections/images/line.png
new file mode 100644
index 0000000..3498cb9
Binary files /dev/null and b/fedora_elections/images/line.png differ
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
new file mode 100644
index 0000000..b19bd33
--- /dev/null
+++ b/fedora_elections/models.py
@@ -0,0 +1,103 @@
+from fedora_elections import app, db
+
+from datetime import datetime
+
+class Election(db.Model):
+ __tablename__ = 'elections'
+
+ id = db.Column(db.Integer, primary_key=True)
+ summary = db.Column(db.Unicode(150), unique=True, nullable=False)
+ alias = db.Column(db.Unicode(100), unique=True, nullable=False)
+ description = db.Column(db.UnicodeText, nullable=False)
+ url = db.Column(db.Unicode(250), nullable=False)
+ start_date = db.Column(db.DateTime, nullable=False)
+ end_date = db.Column(db.DateTime, nullable=False)
+ number_elected = db.Column(db.Integer, nullable=False, default=1)
+ embargoed = db.Column(db.Boolean, nullable=False, default=True)
+ frontpage = db.Column(db.Boolean, nullable=False, default=False)
+ voting_type = db.Column(db.Unicode(100), nullable=False, default=u'range')
+ candidates_are_fasusers = db.Column(db.Boolean, nullable=False,
+ default=False)
+ fas_user = db.Column(db.Unicode(50), nullable=False)
+
+ @property
+ def status(self):
+ if datetime.now() < self.start_date:
+ return 'Pending'
+ else:
+ if datetime.now() < self.end_date:
+ return 'In progress'
+ else:
+ if self.embargoed:
+ return 'Embargoed'
+ else:
+ return 'Ended'
+
+ @property
+ def locked(self):
+ return datetime.now() >= self.start_date
+
+
+class ElectionAdminGroup(db.Model):
+ __tablename__ = 'electionadmingroups'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('admin_groups',
+ lazy='dynamic'))
+ group_name = db.Column(db.Unicode(150), nullable=False)
+ role_required = db.Column(db.Enum(u'user', u'sponsor', u'administrator'),
+ nullable=False)
+
+
+class Candidate(db.Model):
+ __tablename__ = 'candidates'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('candidates',
+ lazy='dynamic'))
+ name = db.Column(db.Unicode(150), nullable=False) # FAS username if
+ # candidates_are_fasusers
+ url = db.Column(db.Unicode(250))
+
+ @property
+ def vote_count(self):
+ values = [v.value for v in self.votes]
+ return sum(values)
+
+
+class LegalVoter(db.Model):
+ __tablename__ = 'legalvoters'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('legal_voters',
+ lazy='dynamic'))
+ # special names:
+ # "cla + one" = cla_done + 1 non-cla group
+ group_name = db.Column(db.Unicode(150), nullable=False)
+
+
+class Vote(db.Model):
+ __tablename__ = 'votes'
+
+ __table_args__ = (db.UniqueConstraint('election_id', 'voter',
+ 'candidate_id',
+ name='eid_voter_cid'), {} )
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('votes',
+ lazy='dynamic'))
+ voter = db.Column(db.Unicode(150), nullable=False)
+ timestamp = db.Column(db.DateTime, nullable=False)
+ candidate_id = db.Column(db.Integer, db.ForeignKey('candidates.id'),
+ nullable=False)
+ candidate = db.relationship('Candidate', backref=db.backref('votes',
+ lazy='dynamic'))
+ value = db.Column(db.Integer, nullable=False)
diff --git a/fedora_elections/redirect.py b/fedora_elections/redirect.py
new file mode 100644
index 0000000..5571abe
--- /dev/null
+++ b/fedora_elections/redirect.py
@@ -0,0 +1,23 @@
+import flask
+
+from urlparse import urljoin, urlparse
+
+
+def is_safe_url(target):
+ ref_url = urlparse(flask.request.host_url)
+ test_url = urlparse(urljoin(flask.request.host_url, target))
+ return test_url.scheme in ('http', 'https') and \
+ ref_url.netloc == test_url.netloc
+
+
+def safe_redirect_back(next=None, fallback=('index', {})):
+ targets = []
+ if next:
+ targets.append(next)
+ if 'next' in flask.request.args and \
+ flask.request.args['next']:
+ targets.append(flask.request.args['next'])
+ targets.append(flask.url_for(fallback[0], **fallback[1]))
+ for target in targets:
+ if is_safe_url(target):
+ return flask.redirect(target)
diff --git a/fedora_elections/static/style.css b/fedora_elections/static/style.css
new file mode 100644
index 0000000..ee09d79
--- /dev/null
+++ b/fedora_elections/static/style.css
@@ -0,0 +1,144 @@
+body {
+ font-family: sans-serif;
+ font-size: 12pt;
+ line-height: 1.35;
+}
+
+#flashes {
+ background-color: #ffa;
+}
+
+nav {
+ background-color: #eee;
+ padding: 0.5em 1em;
+}
+
+nav h2 {
+ display: inline;
+ font-size: inherit;
+ margin-right: 1em;
+}
+
+nav ul {
+ display: inline;
+ list-style: none;
+ padding: 0;
+}
+
+nav li {
+ display: inline;
+ margin-right: .5em;
+}
+
+.clear {
+ clear: both;
+}
+
+/* input elements */
+
+fieldset {
+ border: 0;
+ padding: 0;
+ margin: .5em 0;
+ float: left;
+ clear: both;
+}
+
+.row {
+ margin: .25em 0;
+}
+
+.row label:first-child {
+ width: 8em;
+ margin-right: 0.75em;
+ text-align: right;
+ clear: both;
+ font-weight: bold;
+ display: inline-block;
+ vertical-align: top;
+}
+
+.row textarea {
+ width: 30em;
+ height: 8em;
+}
+
+form .row .errors {
+ list-style: none;
+ margin: .5em 0 1em 10.9375em;
+ padding: 0;
+ font-size: .8em;
+ color: #c00;
+ width: 30em;
+}
+
+div.help {
+ width: 40%;
+ float: right;
+ background-color: #ffa;
+ border-top: 1px solid #ee9;
+ padding: 1em;
+/* font-size: .8em; */
+ margin: .625em 0;
+}
+
+div > *:first-child { margin-top: 0; }
+div > *:last-child { margin-bottom: 0; }
+
+form .buttons.indent {
+ margin-left: 8.75em;
+}
+
+.vthot
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #9C0000;
+}
+
+.vtwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #FFA300;
+}
+
+.vtmedwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #93FF63;
+}
+
+.vtmedcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #00DAFE;
+}
+
+.vtcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #0033FF;
+}
+
+.vtcold
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #000083;
+}
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
new file mode 100644
index 0000000..ff781b7
--- /dev/null
+++ b/fedora_elections/templates/_formhelpers.html
@@ -0,0 +1,13 @@
+{% macro render_field(field, after="") %}
+<div class="row">
+ {{ field.label }}{{ field(**kwargs)|safe }}{% if after %} {{ after }}{% endif %}
+ {% if field.errors %}
+ <ul class="errors">
+ {% for error in field.errors %}
+ <li>{{ error }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+</div>
+{% endmacro %}
+{# vim: set ts=2 sw=2 sta et: #}
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
new file mode 100644
index 0000000..e168ac0
--- /dev/null
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -0,0 +1,28 @@
+{% extends "base.html" %}
+{% block title %}Election administration{% endblock %}
+{% block body %}
+ {% if num_elections > 0 %}
+ <h2>Election administration</h2>
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>
+ <a href="{{ url_for('admin_view_election',
+ election_alias=election.alias) }}">
+ {{ election.summary }}
+ </a>
+ </td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+{%endblock %}
diff --git a/fedora_elections/templates/admin/candidate.html b/fedora_elections/templates/admin/candidate.html
new file mode 100644
index 0000000..a7b2fda
--- /dev/null
+++ b/fedora_elections/templates/admin/candidate.html
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}{{ submit_text }}{% endblock %}
+{% block body %}
+ <h2>{{ submit_text }}</h2>
+
+ <form action="" method="post">
+ <fieldset>
+ {{ render_field(form.name) }}
+ {{ render_field(form.url) }}
+ </fieldset>
+
+ <br class="clear">
+ <p class="buttons">
+ <input type="submit" class="submit positive" value="{{ submit_text }}">
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora_elections/templates/admin/delete_candidate.html b/fedora_elections/templates/admin/delete_candidate.html
new file mode 100644
index 0000000..d764f19
--- /dev/null
+++ b/fedora_elections/templates/admin/delete_candidate.html
@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}Delete candidate{% endblock %}
+{% block body %}
+ <h2>{{ self.title() }}</h2>
+
+ <form action="" method="post">
+ <p>Are you sure you want to delete candidate "{{ candidate.name }}"?</p>
+ <p class="buttons">
+ <input type="submit" value="Delete candidate">
+ <a href="{{ url_for('admin_view_election',
+ election_alias=candidate.election.alias) }}">
+ Cancel
+ </a>
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
new file mode 100644
index 0000000..e46bb3a
--- /dev/null
+++ b/fedora_elections/templates/admin/election_form.html
@@ -0,0 +1,80 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}{{ submit_text }}{% endblock %}
+{% block body %}
+ <h2>{{ self.title() }}</h2>
+
+ <form action="" method="post">
+ <fieldset>
+ {{ render_field(form.summary) }}
+ </fieldset>
+ <div class="help">
+ This is the name of the election that users will first see.</br>
+ Example: <kbd>Fedora Board, November 2010</kbd>
+ </div>
+ <fieldset>
+ {{ render_field(form.alias) }}
+ </fieldset>
+ <div class="help">
+ A short, unique name that will be used for the election's URL.</br>
+ Lowercase letters, numbers, hyphens and underscores only.</br>
+ Example: <kbd>boardf15</kbd>
+ </div>
+ <fieldset>
+ {{ render_field(form.description) }}
+ </fieldset>
+ <fieldset>
+ <div class="row">
+ {{ form.voting_type.label }}
+ {% for subtype in form.voting_type %}
+ {{ subtype }} {{ subtype.label }}
+ {% endfor %}
+ </div>
+ </fieldset>
+ <div class="help">
+ Select the election type.
+ </div>
+ <fieldset>
+ {{ render_field(form.url) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.start_date, " at 00:00:00 UTC") }}
+ {{ render_field(form.end_date, " at 23:59:59 UTC") }}
+ </fieldset>
+ <div class="help">
+ Enter dates in <kbd>YYYY-MM-DD</kbd> format.
+ </div>
+ <fieldset>
+ {{ render_field(form.number_elected) }}
+ </fieldset>
+ <div class="help">
+ This determines the cutoff for how many candidates win the election.
+ For example, if you are electing four empty seats on a committee,
+ enter <kbd>4</kbd> here.
+ </div>
+ <fieldset>
+ {{ render_field(form.candidates_are_fasusers) }}
+ </fieldset>
+ <div class="help">
+ Check this if every candidate in this election has a FAS user account.
+ This will automatically pull full names and wiki page URLs from FAS.
+ </div>
+ <fieldset>
+ {{ render_field(form.frontpage) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.embargoed) }}
+ </fieldset>
+ <div class="help">
+ Embargo results when the election is finished. To unembargo them, edit
+ the election and uncheck this box.
+ </div>
+
+ <br class="clear">
+ {% block election_form_extra %}{% endblock %}
+ <p class="buttons">
+ <input type="submit" class="submit positive" value="Save Changes">
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
new file mode 100644
index 0000000..ed070f3
--- /dev/null
+++ b/fedora_elections/templates/admin/view_election.html
@@ -0,0 +1,42 @@
+{% extends "base.html" %}
+{% block body %}
+ <h2>{{ election.summary }}</h2>
+ <p>{{ election.description }}</p>
+ <p>
+ <a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">
+ Edit election
+ </a>
+ </p>
+ <ul>
+ <li>On front page: {{ election.frontpage }}</li>
+ <li>Start date: {{ election.start_date }}</li>
+ <li>End date: {{ election.end_date }}</li>
+ <li>Number elected: {{ election.number_elected }}</li>
+ <li>Election type: {{ election.voting_type }}</li>
+ </ul>
+ <h3>Candidates</h3>
+ {% if election.candidates.all() %}
+ <ul>
+ {% for candidate in election.candidates.all() %}
+ <li>{{ candidate.name }}
+ <a href="{{ url_for('admin_edit_candidate',
+ election_alias=election.alias,
+ candidate_id=candidate.id) }}">
+ [edit]
+ </a>
+ <a href="{{ url_for('admin_delete_candidate',
+ election_alias=election.alias,
+ candidate_id=candidate.id) }}">
+ [x]
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>There are no candidates.</p>
+ {% endif %}
+ <a href="{{ url_for('admin_add_candidate',
+ election_alias=election.alias) }}">
+ Add a candidate
+ </a>
+{% endblock %}
diff --git a/fedora_elections/templates/auth/login.html b/fedora_elections/templates/auth/login.html
new file mode 100644
index 0000000..9e7b73c
--- /dev/null
+++ b/fedora_elections/templates/auth/login.html
@@ -0,0 +1,20 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}Log in{% endblock %}
+{% block body %}
+ <h2>Log in</h2>
+
+ <form action="" method="post">
+ <p>
+ Log in using your
+ <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a>
+ account.
+ </p>
+ {{ render_field(form.username) }}
+ {{ render_field(form.password) }}
+ <p class="buttons indent"><input type="submit" class="submit positive"
+ value="Log in">
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
new file mode 100644
index 0000000..3e6f806
--- /dev/null
+++ b/fedora_elections/templates/base.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ {% block head %}
+ <title>
+ Fedora Elections
+ {% if self.title() %}
+ —
+ {% block title %}{% endblock %}
+ {% endif %}
+ </title>
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="{{ url_for("static", filename="style.css") }}" />
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="https://fedoraproject.org/static/css/fedora.css" />
+ {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
+ {% endfor %}
+ {% endblock %}
+ </head>
+ <body>
+ <div id="wrapper">
+ <div id="head">
+ <h1><a href="https://admin.fedoraproject.org/en/index">Fedora</a></h1>
+ </div>
+
+ <div id="sidebar">
+ <div id="nav">
+ <h2>Navigation</h2>
+ <ul>
+ <li id="nav-home">
+ <a href="http://fedoraproject.org/en/index">Home</a>
+ </li>
+ <li id="nav-get">
+ <a href="http://fedoraproject.org/en/get-fedora">Get Fedora</a>
+ </li>
+ <li id="nav-join">
+ <a href="http://fedoraproject.org/en/join-fedora">Join Fedora</a>
+ </li>
+ <li id="nav-help">
+ <a href="http://fedoraproject.org/en/get-help">Get Help</a>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <div id="content">
+ <ul class="toolbar" id="#main-toolbar">
+ {% if g.fas_user %}
+ <li>
+ Hello, {{ g.fas_user.username }} —
+ <a href="{{ url_for("auth_logout") }}">log out</a>
+ </li>
+ {% else %}
+ <li>
+ You are not logged in —
+ <a href="{{ url_for("auth_login") }}">log in</a>
+ </li>
+ {% endif %}
+ </ul>
+ <h2>Elections</h2>
+ <ul>
+ <li>
+ <a href="{{ url_for("open_elections") }}">
+ Open elections
+ </a>
+ </li>
+ <li>
+ <a href="{{ url_for("archived_elections") }}">
+ Archived elections
+ </a>
+ </li>
+ </ul>
+ <br>
+ <h2>Administration</h2>
+ <ul>
+ <li>
+ <a href="{{ url_for("admin_view_elections") }}">
+ My elections
+ </a>
+ </li>
+ <li>
+ <a href="{{ url_for("admin_new_election") }}">
+ Create new election
+ </a>
+ </li>
+ </ul>
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+ <ul id="flashes">
+ {% for message in messages %}
+ <li>{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {% endwith %}
+ {% block body %}
+ {% endblock %}
+ </div>
+ </div>
+ <div id="bottom">
+ <div id="footer">
+ {% block footer %}
+ <p class="copy">
+ Copyright © 2008 Red Hat, Inc. and others. Please send any
+ comments or corrections to the
+ <a href="mailto:webmaster@fedoraproject.org">
+ websites team
+ </a>.
+ </p>
+ <p class="disclaimer">
+ The Fedora Project is maintained and driven by the community and
+ sponsored by Red Hat. This is a community maintained site. Red
+ Hat is not responsible for content.
+ </p>
+ <ul>
+ <li class="first">
+ <a href="/en/sponsors">Sponsors</a>
+ </li>
+ <li>
+ <a href="http://fedoraproject.org/wiki/Legal">
+ Legal
+ </a>
+ </li>
+ <li>
+ <a href="http://fedoraproject.org/wiki/Legal/TrademarkGuidelines">
+ Trademark Guidelines
+ </a>
+ </li>
+ </ul>
+ </div>
+ {% endblock %}
+ </div>
+ </body>
+</html>
diff --git a/fedora_elections/templates/election/about.html b/fedora_elections/templates/election/about.html
new file mode 100644
index 0000000..c7bf0ec
--- /dev/null
+++ b/fedora_elections/templates/election/about.html
@@ -0,0 +1,40 @@
+{% extends "base.html" %}
+{% block title %}Election Information{% endblock %}
+{% block body %}
+ <h1>{{ election.summary }}</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ {% if election.status == 'Ended' %}
+ <a href="/results/{{ election.alias }}">Show Results!</a>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p />
+ {% endif %}
+{% endblock %}
diff --git a/fedora_elections/templates/election/archive.html b/fedora_elections/templates/election/archive.html
new file mode 100644
index 0000000..8740b3c
--- /dev/null
+++ b/fedora_elections/templates/election/archive.html
@@ -0,0 +1,28 @@
+{% extends "base.html" %}
+{% block body %}
+ {% if num_elections > 0 %}
+ <h2>Past Elections</h2>
+ <table border="1" cellpadding="1">
+ <tr>
+ <th></th>
+ <th>Start Date (UTC)</th>
+ <th>End Date (UTC)</th>
+ <th></th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{election.description}}</td>
+ <td>{{election.start_date}}</td>
+ <td>{{election.end_date}}</td>
+ <td>
+ <a href="about/{{ election.alias }}"">More information</a>
+ </td>
+ <td>
+ <a href="results/{{ election.alias }}">Results</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+{% endblock %}
diff --git a/fedora_elections/templates/election/results.html b/fedora_elections/templates/election/results.html
new file mode 100644
index 0000000..f61d214
--- /dev/null
+++ b/fedora_elections/templates/election/results.html
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+{% block body %}
+ <h1>{{ election.summary }} Results</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{candidate.name}}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ <td>{{ candidate.vote_count }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+{% endblock %}
diff --git a/fedora_elections/templates/election/vote.html b/fedora_elections/templates/election/vote.html
new file mode 100644
index 0000000..e3c4dd4
--- /dev/null
+++ b/fedora_elections/templates/election/vote.html
@@ -0,0 +1,116 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{election.summary}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
+<form action="" method="post">
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ <a href="{{candidate.url}}">[Info]</a>
+ </td>
+ <!-- If user is logged in: -->
+ {% if g.fas_user %}
+ {% if nextaction == 'vote' %}
+ <td>
+ <select name="{{candidate.id}}">
+ {% for opt in range(0, num_candidates+1) %}
+ <option value={{opt}}>{{opt}}</option>
+ {% endfor %}
+ </select>
+ </td>
+ {% endif %}
+ {% if nextaction == 'confirm' %}
+ <td>
+ {% set cid = candidate.id %}
+ <input type="hidden" name="{{candidate.id}}"
+ value="{{voteinfo[cid]}}" />
+ {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
+ {% if weightpercent >= 90 %}
+ <div class="vthot">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 70 %}
+ <div class="vtwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 50 %}
+ <div class="vtmedwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 30 %}
+ <div class="vtmedcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 10 %}
+ <div class="vtcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% else %}
+ <div class="vtcold">
+ {{voteinfo[cid]}}
+ </div>
+ {% endif %}
+ </td>
+ {% endif %}
+ {% endif %}
+ <!-- End if user logged in -->
+ </tr>
+ {% endfor %}
+ </table>
+ {% if g.fas_user %}
+ <p>
+ {% if nextaction == 'confirm' %}
+ <input type="submit" name="{{nextaction}}" value="Submit" />
+ {% else %}
+ <input type="submit" name="{{nextaction}}" value="Preview" />
+ {% endif %}
+ </p>
+ {% endif %}
+</form>
+
+<!-- Make this conditional on whether the user is logged in and can vote in the election -->
+{% if election.legal_voters.count() != 0 %}
+<p>
+ To vote in this election you must be a member of any one of the following groups:
+</p>
+<ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+</ul>
+{% else %}
+<p />
+{% endif %}
+<div>
+ <h2>Information on Range Voting</h2>
+ <p>
+ Fedora Project has implemented
+ <a href="http://rangevoting.org/">Range Voting</a>
+ for this election, in particular the "Range (score-summing, blanks
+ treated as zero score, no quorum rule)" range voting system.
+ </p>
+ <p>
+ To cast your vote in this election simply select a value between 0 and
+ {{num_candidates}} with 0 as 'least or no preference' and
+ {{num_candidates}} as 'highest preference'.
+ </p>
+ <p>
+ At the end of the election, the highest ranking candidate(s) are marked
+ as the winners.
+ </p>
+ <p>
+ For more information about Range Voting, visit the
+ <a href="http://rangevoting.org/">Center for Range Voting</a>.
+ </p>
+</div>
+{% endblock %}
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
new file mode 100644
index 0000000..290c2cf
--- /dev/null
+++ b/fedora_elections/templates/list/index.html
@@ -0,0 +1,32 @@
+{% extends "base.html" %}
+{% block body %}
+ {% if num_elections > 0 %}
+ <h2>{{ title }}</h2>
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{ election.summary }}</td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ <td>
+ {% if election.status == "In progress" %}
+ {% if g.fas_user %}
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">
+ Vote now!
+ </a>
+ {% endif %}
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+{% endblock %}
commit 5045c40fc5aaa0074ea9a40bd54a623424ddbd4a
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Aug 24 03:38:28 2013 +0000
Restructure.
diff --git a/createdb b/createdb
deleted file mode 100755
index 115635b..0000000
--- a/createdb
+++ /dev/null
@@ -1 +0,0 @@
-FEDORA_ELECTIONS_CONFIG=/etc/fedora-elections/fedora-elections.cfg python createdb.py
diff --git a/createdb.in b/createdb.in
deleted file mode 100644
index c7a87da..0000000
--- a/createdb.in
+++ /dev/null
@@ -1 +0,0 @@
-FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora-elections/fedora-elections.cfg python @DATADIR(a)/fedora-elections/createdb.py
diff --git a/createdb.py b/createdb.py
deleted file mode 100644
index a909c5e..0000000
--- a/createdb.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7']
-import pkg_resources
-
-from fedora_elections import db
-db.create_all()
diff --git a/fedora-elections.cfg.in b/fedora-elections.cfg.in
deleted file mode 100644
index ae68f66..0000000
--- a/fedora-elections.cfg.in
+++ /dev/null
@@ -1,18 +0,0 @@
-import os, os.path
-
-DEBUG = True
-
-
-# You will want to change this for your install
-SECRET_KEY = 'change me'
-SQLALCHEMY_DATABASE_URI = 'sqlite:///@SHAREDSTDIR@/fedora-elections/database.sqlite'
-
-FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
-FAS_USERNAME = 'fcelections'
-FAS_PASSWORD = 'el3cTo$hozer'
-FAS_CHECK_CERT = False
-FAS_HTTPS_REQUIRED = False
-
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
-# List of URLs for additional site-wide CSS
-EXTRA_CSS = []
diff --git a/fedora-elections.cfg.sample b/fedora-elections.cfg.sample
deleted file mode 100644
index 1dff33a..0000000
--- a/fedora-elections.cfg.sample
+++ /dev/null
@@ -1,18 +0,0 @@
-import os, os.path
-
-DEBUG = True
-
-
-# You will want to change this for your install
-SECRET_KEY = 'change me'
-SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.getcwd(), 'database.sqlite')
-
-FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
-FAS_USERNAME = 'elections'
-FAS_PASSWORD ='fake_fas_password'
-FAS_CHECK_CERT = False
-FAS_HTTPS_REQUIRED = False
-
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
-# List of URLs for additional site-wide CSS
-EXTRA_CSS = []
diff --git a/fedora-elections.wsgi.in b/fedora-elections.wsgi.in
deleted file mode 100644
index 430cc5c..0000000
--- a/fedora-elections.wsgi.in
+++ /dev/null
@@ -1,21 +0,0 @@
-#-*- coding: UTF-8 -*-
-
-# These two lines are needed to run on EL6
-import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
-import pkg_resources
-
-import os
-import sys
-
-sys.stdout = sys.stderr
-
-## Set the environment variable pointing to the configuration file
-os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
-
-## The following is only needed if you did not install
-## as a python module (for example if you run it from a git clone).
-import sys
-# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
-
-from fedora_elections import app as application
diff --git a/fedora-elections/__init__.py b/fedora-elections/__init__.py
new file mode 100644
index 0000000..86bfdc6
--- /dev/null
+++ b/fedora-elections/__init__.py
@@ -0,0 +1,481 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2012 Red Hat, Inc.
+# Copyright © 2012 Ian Weller <ianweller(a)fedoraproject.org>
+# Copyright © 2012 Toshio Kuratomi <tkuratom(a)redhat.com>
+# Copyright © 2012 Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+#
+# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
+# Toshio Kuratomi <tkuratom(a)redhat.com>
+# Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+
+import flask
+from flask.ext.fas import FAS
+from flask.ext.sqlalchemy import SQLAlchemy
+
+from fedora.client import AuthError, AppError
+from fedora.client.fas2 import AccountSystem
+
+from sqlalchemy.orm.exc import NoResultFound
+
+import fedmsgshim
+
+from datetime import datetime, time
+from functools import wraps
+from urlparse import urlparse, urljoin
+
+app = flask.Flask(__name__)
+app.config.from_object('fedora_elections.default_config')
+app.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
+
+# set up SQLAlchemy
+db = SQLAlchemy(app)
+
+# set up FAS
+fas = FAS(app)
+
+# FAS for usernames.
+# baseURL = "https://admin.fedoraproject.org/accounts"
+# fasuser = "elections"
+# faspw = "elections"
+# fas2_url = 'http://fakefas.fedoraproject.org/accounts/'
+fas2_url = app.config['FAS_BASE_URL']
+fas2_username = app.config['FAS_USERNAME']
+fas2_password = app.config['FAS_PASSWORD']
+fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password,
+ insecure=True)
+
+
+# modular imports
+from fedora_elections import forms, models, redirect
+
+
+def remove_csrf(form_data):
+ return dict([(k, v) for k, v in form_data.items() if k != 'csrf'])
+
+
+def login_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if flask.g.fas_user is None:
+ return flask.redirect(flask.url_for('auth_login',
+ next=flask.request.url))
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+def election_admin_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if flask.g.fas_user is None:
+ return flask.redirect(flask.url_for('auth_login',
+ next=flask.request.url))
+ if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
+ flask.g.fas_user.groups:
+ flask.abort(403)
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+### LIST VIEWS #############################################
+
+(a)app.route('/')
+def index():
+ elections = models.Election.query.filter_by(frontpage=True).all()
+ num_elections = len(elections)
+ return flask.render_template('list/index.html', elections=elections,
+ num_elections=num_elections,
+ title="Elections")
+
+
+(a)app.route('/about/<election_alias>')
+def about_election(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/about.html', election=election,
+ usernamemap=usernamemap)
+
+
+(a)app.route('/archive')
+def archived_elections():
+ now = datetime.utcnow()
+
+ try:
+ elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date<now, models.Election.id>0).all()
+ except NoResultFound:
+ flask.flash('There are no archived elections.')
+ return redirect.safe_redirect_back()
+
+ num_elections = len(elections)
+ if (num_elections == 0):
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('election/archive.html',
+ num_elections=num_elections,
+ elections=elections)
+
+
+(a)app.route('/open')
+def open_elections():
+ now = datetime.utcnow()
+
+ try:
+ elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date>now,
+ models.Election.id>0).all()
+ except NoResultFound:
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ num_elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date>now,
+ models.Election.id>0).count()
+ if (num_elections == 0):
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('list/index.html', elections=elections,
+ num_elections=num_elections,
+ title='Open Elections')
+
+
+### ELECTION VIEWS #############################################
+
+(a)app.route('/vote/<election_alias>', methods=['GET', 'POST'])
+def vote(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'Pending':
+ flask.flash('Voting has not yet started, sorry.')
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Ended':
+ flask.flash('This election is closed. You have been redirected to ' +
+ 'the election results.')
+ return flask.redirect(flask.url_for('results', election.shortname))
+
+ votes = models.Vote.query.filter_by(election_id=election.id,
+ voter=flask.g.fas_user.username).count()
+ if (votes != 0):
+ flask.flash('You have already voted in the election!')
+ return redirect.safe_redirect_back()
+
+ num_candidates = election.candidates.count()
+
+ uvotes = {}
+ for candidate in election.candidates:
+ uvotes[candidate.id] = ""
+ next_action = 'vote'
+
+ if flask.request.method == 'POST':
+ next_action = ''
+ form_values = flask.request.form.values()
+ if 'Submit' in form_values:
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote >= 0 and vote <= num_candidates:
+ uvotes[candidate.id] = int(vote)
+ else:
+ flask.flash("Invalid Ballot!")
+ return redirect.safe_redirect_back()
+
+ except ValueError:
+ flask.flash("Invalid Ballot!")
+ return redirect.safe_redirect_back()
+
+ for uvote in uvotes:
+ new_vote = models.Vote(election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=uvote,
+ value=uvotes[uvote] )
+ db.session.add(new_vote)
+ db.session.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return redirect.safe_redirect_back()
+
+ elif 'Preview' in form_values:
+ flask.flash("Please confirm your vote!")
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote > num_candidates:
+ flask.flash("Invalid data.")
+ uvotes[candidate.id] = 0
+ next_action = 'vote'
+ elif vote >= 0:
+ uvotes[candidate.id] = vote
+ else:
+ flask.flash("Invalid data2.")
+ uvotes[c.id] = 0
+ next_action = 'vote'
+ except ValueError:
+ flask.flash("Invalid data")
+
+ if (next_action != 'vote'):
+ next_action = 'confirm'
+
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/vote.html', election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ voteinfo=uvotes, nextaction=next_action)
+
+
+### AUTH VIEWS #############################################
+
+(a)app.route('/login', methods=('GET', 'POST'))
+def auth_login():
+ if flask.g.fas_user:
+ return redirect.safe_redirect_back()
+ form = forms.LoginForm()
+ if form.validate_on_submit():
+ if fas.login(form.username.data, form.password.data):
+ flask.flash('Welcome, %s' % flask.g.fas_user.username)
+ return redirect.safe_redirect_back()
+ else:
+ flask.flash('Incorrect username or password')
+ return flask.render_template('auth/login.html', form=form)
+
+
+(a)app.route('/logout')
+def auth_logout():
+ if not flask.g.fas_user:
+ return redirect.safe_redirect_back()
+ fas.logout()
+ flask.flash('You have been logged out')
+ return redirect.safe_redirect_back()
+
+
+### ELECTION ADMIN VIEWS ###################################
+
+(a)app.route('/admin/')
+@election_admin_required
+def admin_view_elections():
+ elections = models.Election.query.\
+ filter_by(fas_user=flask.g.fas_user.username).all()
+ num_elections = len(elections)
+ if (num_elections == 0):
+ flask.flash('You do not have any elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('admin/all_elections.html',
+ num_elections=num_elections,
+ elections=elections)
+
+
+(a)app.route('/admin/new', methods=("GET", "POST"))
+@election_admin_required
+def admin_new_election():
+ form = forms.ElectionForm()
+ if form.validate_on_submit():
+ election = models.Election(**remove_csrf(form.data))
+ # Fix start_date and end_date to use datetime
+ election.start_date = datetime.combine(election.start_date, time())
+ election.end_date = datetime.combine(election.end_date,
+ time(23, 59, 59))
+ election.fas_user = flask.g.fas_user.username
+ db.session.add(election)
+ admin = models.ElectionAdminGroup(election=election,
+ group_name=app.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ role_required=u'user')
+ db.session.add(admin)
+ db.session.commit()
+ flask.flash('Election "%s" added' % election.summary)
+ fedmsgshim.publish(topic="election.new", msg=election)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/election_form.html', form=form,
+ submit_text='Create election')
+
+
+(a)app.route('/admin/<election_alias>/')
+@election_admin_required
+def admin_view_election(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ return flask.render_template('admin/view_election.html', election=election)
+
+
+(a)app.route('/admin/<election_alias>/edit', methods=("GET", "POST"))
+@election_admin_required
+def admin_edit_election(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ form = forms.ElectionForm(election.id, obj=election)
+ if form.validate_on_submit():
+ form.populate_obj(election)
+ db.session.commit()
+ flask.flash('Election "%s" saved' % election.summary)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/election_form.html', form=form,
+ submit_text='Edit election')
+
+
+(a)app.route('/admin/<election_alias>/candidates/new', methods=("GET", "POST"))
+@election_admin_required
+def admin_add_candidate(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ form = forms.CandidateForm()
+ if form.validate_on_submit():
+ candidate = models.Candidate(election=election,
+ **remove_csrf(form.data))
+ db.session.add(candidate)
+ db.session.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/candidate.html', form=form,
+ submit_text='Add candidate')
+
+
+(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
+ methods=("GET", "POST"))
+@election_admin_required
+def admin_edit_candidate(election_alias, candidate_id):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ if not election or not candidate:
+ flask.abort(404)
+ if candidate.election != election:
+ flask.abort(404)
+ form = forms.CandidateForm(obj=candidate)
+ if form.validate_on_submit():
+ form.populate_obj(candidate)
+ db.session.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/candidate.html', form=form,
+ submit_text='Edit candidate')
+
+
+(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
+ methods=("GET", "POST"))
+@election_admin_required
+def admin_delete_candidate(election_alias, candidate_id):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ if not election or not candidate:
+ flask.abort(404)
+ if candidate.election != election:
+ flask.abort(404)
+ form = forms.ConfirmationForm()
+ if form.validate_on_submit():
+ db.session.delete(candidate)
+ candidate_name = candidate.name
+ db.session.commit()
+ flask.flash('Candidate "%s" deleted' % candidate_name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/delete_candidate.html', form=form,
+ candidate=candidate)
+
+
+(a)app.route('/results/<election_alias>')
+def election_results(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'In progress':
+ flask.flash("We are sorry. The results for this election cannot be" \
+ " viewed at this time because the election is still in" \
+ " progress.")
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Pending':
+ flask.flash("We are sorry. The results for this election cannot be" \
+ " viewed at this time because the election has not" \
+ " started.")
+ return redirect.safe_redirect_back()
+
+ elif election.embargoed == 1:
+ if flask.g.fas_user is None:
+ flask.flash("We are sorry. The results for this election" \
+ "cannot be viewed because they are currently" \
+ " embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
+ else:
+ if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
+ flask.g.fas_user.groups:
+ pass
+ else:
+ match = 0
+ admingroups = models.ElectionAdminGroup.query.filter_by(
+ election_id=election.id).all()
+ for admingroup in admingroups:
+ if admingroup.group_name in flask.g.fas_user.groups:
+ match = 1
+
+ if match == 0:
+ flask.flash("We are sorry. The results for this election" \
+ "cannot be viewed because they are currently" \
+ " embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/results.html', election=election,
+ usernamemap=usernamemap)
diff --git a/fedora-elections/createdb.in b/fedora-elections/createdb.in
new file mode 100644
index 0000000..c7a87da
--- /dev/null
+++ b/fedora-elections/createdb.in
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora-elections/fedora-elections.cfg python @DATADIR(a)/fedora-elections/createdb.py
diff --git a/fedora-elections/createdb.py b/fedora-elections/createdb.py
new file mode 100644
index 0000000..a909c5e
--- /dev/null
+++ b/fedora-elections/createdb.py
@@ -0,0 +1,6 @@
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7']
+import pkg_resources
+
+from fedora_elections import db
+db.create_all()
diff --git a/fedora-elections/default_config.py b/fedora-elections/default_config.py
new file mode 100644
index 0000000..7b5b794
--- /dev/null
+++ b/fedora-elections/default_config.py
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
diff --git a/fedora-elections/fedmsgshim.py b/fedora-elections/fedmsgshim.py
new file mode 100644
index 0000000..775f785
--- /dev/null
+++ b/fedora-elections/fedmsgshim.py
@@ -0,0 +1,17 @@
+""" This is a temporary shim that allows fedmsg to be an optional dependency of
+elections. If fedmsg is installed, these function calls will try to actually send
+messages. If it is not installed, it will return silently.
+
+ :Author: Ralph Bean <rbean(a)redhat.com>
+
+"""
+
+import warnings
+
+
+def publish(*args, **kwargs):
+ try:
+ import fedmsg
+ fedmsg.publish(*args, **kwargs)
+ except Exception, e:
+ warnings.warn(str(e))
diff --git a/fedora-elections/fedora-elections.cfg.in b/fedora-elections/fedora-elections.cfg.in
new file mode 100644
index 0000000..ae68f66
--- /dev/null
+++ b/fedora-elections/fedora-elections.cfg.in
@@ -0,0 +1,18 @@
+import os, os.path
+
+DEBUG = True
+
+
+# You will want to change this for your install
+SECRET_KEY = 'change me'
+SQLALCHEMY_DATABASE_URI = 'sqlite:///@SHAREDSTDIR@/fedora-elections/database.sqlite'
+
+FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
+FAS_USERNAME = 'fcelections'
+FAS_PASSWORD = 'el3cTo$hozer'
+FAS_CHECK_CERT = False
+FAS_HTTPS_REQUIRED = False
+
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+# List of URLs for additional site-wide CSS
+EXTRA_CSS = []
diff --git a/fedora-elections/fedora-elections.wsgi.in b/fedora-elections/fedora-elections.wsgi.in
new file mode 100644
index 0000000..430cc5c
--- /dev/null
+++ b/fedora-elections/fedora-elections.wsgi.in
@@ -0,0 +1,21 @@
+#-*- coding: UTF-8 -*-
+
+# These two lines are needed to run on EL6
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import os
+import sys
+
+sys.stdout = sys.stderr
+
+## Set the environment variable pointing to the configuration file
+os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
+
+## The following is only needed if you did not install
+## as a python module (for example if you run it from a git clone).
+import sys
+# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
+
+from fedora_elections import app as application
diff --git a/fedora-elections/forms.py b/fedora-elections/forms.py
new file mode 100644
index 0000000..f050dbd
--- /dev/null
+++ b/fedora-elections/forms.py
@@ -0,0 +1,69 @@
+from flask.ext import wtf
+
+from fedora_elections.models import Election
+
+
+class ElectionForm(wtf.Form):
+ summary = wtf.TextField('Summary', [wtf.validators.Required(),
+ wtf.validators.Length(max=150)])
+ alias = wtf.TextField('Alias', [
+ wtf.validators.Required(),
+ wtf.validators.Length(max=100),
+ wtf.validators.Regexp('[a-z0-9_-]+',
+ message=('Alias may only contain lower case '
+ 'letters, numbers, hyphens and '
+ 'underscores.')),
+ ])
+ description = wtf.TextAreaField('Description', [wtf.validators.Required()])
+ url = wtf.TextField('URL', [wtf.validators.Required(),
+ wtf.validators.URL(),
+ wtf.validators.Length(max=250)])
+ start_date = wtf.DateField('Start date', [wtf.validators.Required()])
+ end_date = wtf.DateField('End date', [wtf.validators.Required()])
+ number_elected = wtf.IntegerField('Number elected',
+ [wtf.validators.Required(),
+ wtf.validators.NumberRange(min=1)],
+ default=1)
+ candidates_are_fasusers = wtf.BooleanField('Candidates are FAS users?')
+ frontpage = wtf.BooleanField('Show on front page?')
+ embargoed = wtf.BooleanField('Embargo results?', default=True)
+
+ def __init__(form, election_id=None, *args, **kwargs):
+ super(ElectionForm, form).__init__(*args, **kwargs)
+ form._election_id = election_id
+
+ def validate_summary(form, field):
+ check = Election.query.filter_by(summary=form.summary.data).all()
+ if check:
+ if not (form._election_id and form._election_id == check[0].id):
+ raise wtf.ValidationError('There is already another election '
+ 'with this summary.')
+
+ def validate_alias(form, field):
+ if form.alias.data == 'new':
+ raise wtf.ValidationError(flask.Markup('The alias cannot be '
+ '<code>new</code>.'))
+ check = Election.query.filter_by(alias=form.alias.data).all()
+ if check:
+ if not (form._election_id and form._election_id == check[0].id):
+ raise wtf.ValidationError('There is already another election '
+ 'with this alias.')
+
+ def validate_end_date(form, field):
+ if form.end_date.data <= form.start_date.data:
+ raise wtf.ValidationError('End date must be later than '
+ 'start date.')
+
+
+class CandidateForm(wtf.Form):
+ name = wtf.TextField('Name', [wtf.validators.Required()])
+ url = wtf.TextField('URL', [wtf.validators.Length(max=250)])
+
+
+class LoginForm(wtf.Form):
+ username = wtf.TextField('Username', [wtf.validators.Required()])
+ password = wtf.PasswordField('Password', [wtf.validators.Required()])
+
+
+class ConfirmationForm(wtf.Form):
+ pass
diff --git a/fedora-elections/httpd-fedora-elections.conf.in b/fedora-elections/httpd-fedora-elections.conf.in
new file mode 100644
index 0000000..c974b88
--- /dev/null
+++ b/fedora-elections/httpd-fedora-elections.conf.in
@@ -0,0 +1,23 @@
+Alias /fedora-elections/images @DATADIR@/fedora-elections/images
+Alias /fedora-elections/static @DATADIR@/fedora-elections/static
+
+WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
+WSGISocketPrefix run/wsgi
+WSGIRestrictStdout On
+WSGIRestrictSignal Off
+WSGIPythonOptimize 1
+
+WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora-elections/fedora-elections.wsgi
+
+<Location /fedora-elections>
+ WSGIProcessGroup fedora-elections
+ <IfModule mod_authz_core.c>
+ # Apache 2.4
+ Require all granted
+ </IfModule>
+ <IfModule !mod_authz_core.c>
+ # Apache 2.2
+ Order deny,allow
+ Allow from all
+ </IfModule>
+</Location>
diff --git a/fedora-elections/images/border-left.png b/fedora-elections/images/border-left.png
new file mode 100644
index 0000000..6f256df
Binary files /dev/null and b/fedora-elections/images/border-left.png differ
diff --git a/fedora-elections/images/border-right.png b/fedora-elections/images/border-right.png
new file mode 100644
index 0000000..0e7365b
Binary files /dev/null and b/fedora-elections/images/border-right.png differ
diff --git a/fedora-elections/images/fedora-logo.png b/fedora-elections/images/fedora-logo.png
new file mode 100644
index 0000000..ce95893
Binary files /dev/null and b/fedora-elections/images/fedora-logo.png differ
diff --git a/fedora-elections/images/line.png b/fedora-elections/images/line.png
new file mode 100644
index 0000000..3498cb9
Binary files /dev/null and b/fedora-elections/images/line.png differ
diff --git a/fedora-elections/models.py b/fedora-elections/models.py
new file mode 100644
index 0000000..b19bd33
--- /dev/null
+++ b/fedora-elections/models.py
@@ -0,0 +1,103 @@
+from fedora_elections import app, db
+
+from datetime import datetime
+
+class Election(db.Model):
+ __tablename__ = 'elections'
+
+ id = db.Column(db.Integer, primary_key=True)
+ summary = db.Column(db.Unicode(150), unique=True, nullable=False)
+ alias = db.Column(db.Unicode(100), unique=True, nullable=False)
+ description = db.Column(db.UnicodeText, nullable=False)
+ url = db.Column(db.Unicode(250), nullable=False)
+ start_date = db.Column(db.DateTime, nullable=False)
+ end_date = db.Column(db.DateTime, nullable=False)
+ number_elected = db.Column(db.Integer, nullable=False, default=1)
+ embargoed = db.Column(db.Boolean, nullable=False, default=True)
+ frontpage = db.Column(db.Boolean, nullable=False, default=False)
+ voting_type = db.Column(db.Unicode(100), nullable=False, default=u'range')
+ candidates_are_fasusers = db.Column(db.Boolean, nullable=False,
+ default=False)
+ fas_user = db.Column(db.Unicode(50), nullable=False)
+
+ @property
+ def status(self):
+ if datetime.now() < self.start_date:
+ return 'Pending'
+ else:
+ if datetime.now() < self.end_date:
+ return 'In progress'
+ else:
+ if self.embargoed:
+ return 'Embargoed'
+ else:
+ return 'Ended'
+
+ @property
+ def locked(self):
+ return datetime.now() >= self.start_date
+
+
+class ElectionAdminGroup(db.Model):
+ __tablename__ = 'electionadmingroups'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('admin_groups',
+ lazy='dynamic'))
+ group_name = db.Column(db.Unicode(150), nullable=False)
+ role_required = db.Column(db.Enum(u'user', u'sponsor', u'administrator'),
+ nullable=False)
+
+
+class Candidate(db.Model):
+ __tablename__ = 'candidates'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('candidates',
+ lazy='dynamic'))
+ name = db.Column(db.Unicode(150), nullable=False) # FAS username if
+ # candidates_are_fasusers
+ url = db.Column(db.Unicode(250))
+
+ @property
+ def vote_count(self):
+ values = [v.value for v in self.votes]
+ return sum(values)
+
+
+class LegalVoter(db.Model):
+ __tablename__ = 'legalvoters'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('legal_voters',
+ lazy='dynamic'))
+ # special names:
+ # "cla + one" = cla_done + 1 non-cla group
+ group_name = db.Column(db.Unicode(150), nullable=False)
+
+
+class Vote(db.Model):
+ __tablename__ = 'votes'
+
+ __table_args__ = (db.UniqueConstraint('election_id', 'voter',
+ 'candidate_id',
+ name='eid_voter_cid'), {} )
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('votes',
+ lazy='dynamic'))
+ voter = db.Column(db.Unicode(150), nullable=False)
+ timestamp = db.Column(db.DateTime, nullable=False)
+ candidate_id = db.Column(db.Integer, db.ForeignKey('candidates.id'),
+ nullable=False)
+ candidate = db.relationship('Candidate', backref=db.backref('votes',
+ lazy='dynamic'))
+ value = db.Column(db.Integer, nullable=False)
diff --git a/fedora-elections/redirect.py b/fedora-elections/redirect.py
new file mode 100644
index 0000000..5571abe
--- /dev/null
+++ b/fedora-elections/redirect.py
@@ -0,0 +1,23 @@
+import flask
+
+from urlparse import urljoin, urlparse
+
+
+def is_safe_url(target):
+ ref_url = urlparse(flask.request.host_url)
+ test_url = urlparse(urljoin(flask.request.host_url, target))
+ return test_url.scheme in ('http', 'https') and \
+ ref_url.netloc == test_url.netloc
+
+
+def safe_redirect_back(next=None, fallback=('index', {})):
+ targets = []
+ if next:
+ targets.append(next)
+ if 'next' in flask.request.args and \
+ flask.request.args['next']:
+ targets.append(flask.request.args['next'])
+ targets.append(flask.url_for(fallback[0], **fallback[1]))
+ for target in targets:
+ if is_safe_url(target):
+ return flask.redirect(target)
diff --git a/fedora-elections/static/style.css b/fedora-elections/static/style.css
new file mode 100644
index 0000000..ee09d79
--- /dev/null
+++ b/fedora-elections/static/style.css
@@ -0,0 +1,144 @@
+body {
+ font-family: sans-serif;
+ font-size: 12pt;
+ line-height: 1.35;
+}
+
+#flashes {
+ background-color: #ffa;
+}
+
+nav {
+ background-color: #eee;
+ padding: 0.5em 1em;
+}
+
+nav h2 {
+ display: inline;
+ font-size: inherit;
+ margin-right: 1em;
+}
+
+nav ul {
+ display: inline;
+ list-style: none;
+ padding: 0;
+}
+
+nav li {
+ display: inline;
+ margin-right: .5em;
+}
+
+.clear {
+ clear: both;
+}
+
+/* input elements */
+
+fieldset {
+ border: 0;
+ padding: 0;
+ margin: .5em 0;
+ float: left;
+ clear: both;
+}
+
+.row {
+ margin: .25em 0;
+}
+
+.row label:first-child {
+ width: 8em;
+ margin-right: 0.75em;
+ text-align: right;
+ clear: both;
+ font-weight: bold;
+ display: inline-block;
+ vertical-align: top;
+}
+
+.row textarea {
+ width: 30em;
+ height: 8em;
+}
+
+form .row .errors {
+ list-style: none;
+ margin: .5em 0 1em 10.9375em;
+ padding: 0;
+ font-size: .8em;
+ color: #c00;
+ width: 30em;
+}
+
+div.help {
+ width: 40%;
+ float: right;
+ background-color: #ffa;
+ border-top: 1px solid #ee9;
+ padding: 1em;
+/* font-size: .8em; */
+ margin: .625em 0;
+}
+
+div > *:first-child { margin-top: 0; }
+div > *:last-child { margin-bottom: 0; }
+
+form .buttons.indent {
+ margin-left: 8.75em;
+}
+
+.vthot
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #9C0000;
+}
+
+.vtwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #FFA300;
+}
+
+.vtmedwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #93FF63;
+}
+
+.vtmedcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #00DAFE;
+}
+
+.vtcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #0033FF;
+}
+
+.vtcold
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #000083;
+}
diff --git a/fedora-elections/templates/_formhelpers.html b/fedora-elections/templates/_formhelpers.html
new file mode 100644
index 0000000..ff781b7
--- /dev/null
+++ b/fedora-elections/templates/_formhelpers.html
@@ -0,0 +1,13 @@
+{% macro render_field(field, after="") %}
+<div class="row">
+ {{ field.label }}{{ field(**kwargs)|safe }}{% if after %} {{ after }}{% endif %}
+ {% if field.errors %}
+ <ul class="errors">
+ {% for error in field.errors %}
+ <li>{{ error }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+</div>
+{% endmacro %}
+{# vim: set ts=2 sw=2 sta et: #}
diff --git a/fedora-elections/templates/admin/all_elections.html b/fedora-elections/templates/admin/all_elections.html
new file mode 100644
index 0000000..e168ac0
--- /dev/null
+++ b/fedora-elections/templates/admin/all_elections.html
@@ -0,0 +1,28 @@
+{% extends "base.html" %}
+{% block title %}Election administration{% endblock %}
+{% block body %}
+ {% if num_elections > 0 %}
+ <h2>Election administration</h2>
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>
+ <a href="{{ url_for('admin_view_election',
+ election_alias=election.alias) }}">
+ {{ election.summary }}
+ </a>
+ </td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+{%endblock %}
diff --git a/fedora-elections/templates/admin/candidate.html b/fedora-elections/templates/admin/candidate.html
new file mode 100644
index 0000000..a7b2fda
--- /dev/null
+++ b/fedora-elections/templates/admin/candidate.html
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}{{ submit_text }}{% endblock %}
+{% block body %}
+ <h2>{{ submit_text }}</h2>
+
+ <form action="" method="post">
+ <fieldset>
+ {{ render_field(form.name) }}
+ {{ render_field(form.url) }}
+ </fieldset>
+
+ <br class="clear">
+ <p class="buttons">
+ <input type="submit" class="submit positive" value="{{ submit_text }}">
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora-elections/templates/admin/delete_candidate.html b/fedora-elections/templates/admin/delete_candidate.html
new file mode 100644
index 0000000..d764f19
--- /dev/null
+++ b/fedora-elections/templates/admin/delete_candidate.html
@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}Delete candidate{% endblock %}
+{% block body %}
+ <h2>{{ self.title() }}</h2>
+
+ <form action="" method="post">
+ <p>Are you sure you want to delete candidate "{{ candidate.name }}"?</p>
+ <p class="buttons">
+ <input type="submit" value="Delete candidate">
+ <a href="{{ url_for('admin_view_election',
+ election_alias=candidate.election.alias) }}">
+ Cancel
+ </a>
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora-elections/templates/admin/election_form.html b/fedora-elections/templates/admin/election_form.html
new file mode 100644
index 0000000..6e2a3d6
--- /dev/null
+++ b/fedora-elections/templates/admin/election_form.html
@@ -0,0 +1,77 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}{{ submit_text }}{% endblock %}
+{% block body %}
+ <h2>{{ self.title() }}</h2>
+
+ <form action="" method="post">
+ <fieldset>
+ {{ render_field(form.summary) }}
+ </fieldset>
+ <div class="help">
+ <p>This is the name of the election that users will first see.</p>
+ <p>Example: <kbd>Fedora Board, November 2010</kbd></p>
+ </div>
+ <fieldset>
+ {{ render_field(form.alias) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ A short, unique name that will be used for the election's URL.
+ Lowercase letters, numbers, hyphens and underscores only.
+ </p>
+ <p>Example: <kbd>boardf15</kbd></p>
+ </div>
+ <fieldset>
+ {{ render_field(form.description) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.url) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.start_date, " at 00:00:00 UTC") }}
+ {{ render_field(form.end_date, " at 23:59:59 UTC") }}
+ </fieldset>
+ <div class="help">
+ <p>Enter dates in <kbd>YYYY-MM-DD</kbd> format.</p>
+ </div>
+ <fieldset>
+ {{ render_field(form.number_elected) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ This determines the cutoff for how many candidates win the election.
+ For example, if you are electing four empty seats on a committee,
+ enter <kbd>4</kbd> here.
+ </p>
+ </div>
+ <fieldset>
+ {{ render_field(form.candidates_are_fasusers) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ Check this if every candidate in this election has a FAS user account.
+ This will automatically pull full names and wiki page URLs from FAS.
+ </p>
+ </div>
+ <fieldset>
+ {{ render_field(form.frontpage) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.embargoed) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ Embargo results when the election is finished. To unembargo them, edit
+ the election and uncheck this box.
+ </p>
+ </div>
+
+ <br class="clear">
+ {% block election_form_extra %}{% endblock %}
+ <p class="buttons">
+ <input type="submit" class="submit positive" value="Save Changes">
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora-elections/templates/admin/view_election.html b/fedora-elections/templates/admin/view_election.html
new file mode 100644
index 0000000..ed070f3
--- /dev/null
+++ b/fedora-elections/templates/admin/view_election.html
@@ -0,0 +1,42 @@
+{% extends "base.html" %}
+{% block body %}
+ <h2>{{ election.summary }}</h2>
+ <p>{{ election.description }}</p>
+ <p>
+ <a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">
+ Edit election
+ </a>
+ </p>
+ <ul>
+ <li>On front page: {{ election.frontpage }}</li>
+ <li>Start date: {{ election.start_date }}</li>
+ <li>End date: {{ election.end_date }}</li>
+ <li>Number elected: {{ election.number_elected }}</li>
+ <li>Election type: {{ election.voting_type }}</li>
+ </ul>
+ <h3>Candidates</h3>
+ {% if election.candidates.all() %}
+ <ul>
+ {% for candidate in election.candidates.all() %}
+ <li>{{ candidate.name }}
+ <a href="{{ url_for('admin_edit_candidate',
+ election_alias=election.alias,
+ candidate_id=candidate.id) }}">
+ [edit]
+ </a>
+ <a href="{{ url_for('admin_delete_candidate',
+ election_alias=election.alias,
+ candidate_id=candidate.id) }}">
+ [x]
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>There are no candidates.</p>
+ {% endif %}
+ <a href="{{ url_for('admin_add_candidate',
+ election_alias=election.alias) }}">
+ Add a candidate
+ </a>
+{% endblock %}
diff --git a/fedora-elections/templates/auth/login.html b/fedora-elections/templates/auth/login.html
new file mode 100644
index 0000000..9e7b73c
--- /dev/null
+++ b/fedora-elections/templates/auth/login.html
@@ -0,0 +1,20 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}Log in{% endblock %}
+{% block body %}
+ <h2>Log in</h2>
+
+ <form action="" method="post">
+ <p>
+ Log in using your
+ <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a>
+ account.
+ </p>
+ {{ render_field(form.username) }}
+ {{ render_field(form.password) }}
+ <p class="buttons indent"><input type="submit" class="submit positive"
+ value="Log in">
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora-elections/templates/base.html b/fedora-elections/templates/base.html
new file mode 100644
index 0000000..3e6f806
--- /dev/null
+++ b/fedora-elections/templates/base.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ {% block head %}
+ <title>
+ Fedora Elections
+ {% if self.title() %}
+ —
+ {% block title %}{% endblock %}
+ {% endif %}
+ </title>
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="{{ url_for("static", filename="style.css") }}" />
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="https://fedoraproject.org/static/css/fedora.css" />
+ {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
+ {% endfor %}
+ {% endblock %}
+ </head>
+ <body>
+ <div id="wrapper">
+ <div id="head">
+ <h1><a href="https://admin.fedoraproject.org/en/index">Fedora</a></h1>
+ </div>
+
+ <div id="sidebar">
+ <div id="nav">
+ <h2>Navigation</h2>
+ <ul>
+ <li id="nav-home">
+ <a href="http://fedoraproject.org/en/index">Home</a>
+ </li>
+ <li id="nav-get">
+ <a href="http://fedoraproject.org/en/get-fedora">Get Fedora</a>
+ </li>
+ <li id="nav-join">
+ <a href="http://fedoraproject.org/en/join-fedora">Join Fedora</a>
+ </li>
+ <li id="nav-help">
+ <a href="http://fedoraproject.org/en/get-help">Get Help</a>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <div id="content">
+ <ul class="toolbar" id="#main-toolbar">
+ {% if g.fas_user %}
+ <li>
+ Hello, {{ g.fas_user.username }} —
+ <a href="{{ url_for("auth_logout") }}">log out</a>
+ </li>
+ {% else %}
+ <li>
+ You are not logged in —
+ <a href="{{ url_for("auth_login") }}">log in</a>
+ </li>
+ {% endif %}
+ </ul>
+ <h2>Elections</h2>
+ <ul>
+ <li>
+ <a href="{{ url_for("open_elections") }}">
+ Open elections
+ </a>
+ </li>
+ <li>
+ <a href="{{ url_for("archived_elections") }}">
+ Archived elections
+ </a>
+ </li>
+ </ul>
+ <br>
+ <h2>Administration</h2>
+ <ul>
+ <li>
+ <a href="{{ url_for("admin_view_elections") }}">
+ My elections
+ </a>
+ </li>
+ <li>
+ <a href="{{ url_for("admin_new_election") }}">
+ Create new election
+ </a>
+ </li>
+ </ul>
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+ <ul id="flashes">
+ {% for message in messages %}
+ <li>{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {% endwith %}
+ {% block body %}
+ {% endblock %}
+ </div>
+ </div>
+ <div id="bottom">
+ <div id="footer">
+ {% block footer %}
+ <p class="copy">
+ Copyright © 2008 Red Hat, Inc. and others. Please send any
+ comments or corrections to the
+ <a href="mailto:webmaster@fedoraproject.org">
+ websites team
+ </a>.
+ </p>
+ <p class="disclaimer">
+ The Fedora Project is maintained and driven by the community and
+ sponsored by Red Hat. This is a community maintained site. Red
+ Hat is not responsible for content.
+ </p>
+ <ul>
+ <li class="first">
+ <a href="/en/sponsors">Sponsors</a>
+ </li>
+ <li>
+ <a href="http://fedoraproject.org/wiki/Legal">
+ Legal
+ </a>
+ </li>
+ <li>
+ <a href="http://fedoraproject.org/wiki/Legal/TrademarkGuidelines">
+ Trademark Guidelines
+ </a>
+ </li>
+ </ul>
+ </div>
+ {% endblock %}
+ </div>
+ </body>
+</html>
diff --git a/fedora-elections/templates/election/about.html b/fedora-elections/templates/election/about.html
new file mode 100644
index 0000000..c7bf0ec
--- /dev/null
+++ b/fedora-elections/templates/election/about.html
@@ -0,0 +1,40 @@
+{% extends "base.html" %}
+{% block title %}Election Information{% endblock %}
+{% block body %}
+ <h1>{{ election.summary }}</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ {% if election.status == 'Ended' %}
+ <a href="/results/{{ election.alias }}">Show Results!</a>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p />
+ {% endif %}
+{% endblock %}
diff --git a/fedora-elections/templates/election/archive.html b/fedora-elections/templates/election/archive.html
new file mode 100644
index 0000000..8740b3c
--- /dev/null
+++ b/fedora-elections/templates/election/archive.html
@@ -0,0 +1,28 @@
+{% extends "base.html" %}
+{% block body %}
+ {% if num_elections > 0 %}
+ <h2>Past Elections</h2>
+ <table border="1" cellpadding="1">
+ <tr>
+ <th></th>
+ <th>Start Date (UTC)</th>
+ <th>End Date (UTC)</th>
+ <th></th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{election.description}}</td>
+ <td>{{election.start_date}}</td>
+ <td>{{election.end_date}}</td>
+ <td>
+ <a href="about/{{ election.alias }}"">More information</a>
+ </td>
+ <td>
+ <a href="results/{{ election.alias }}">Results</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+{% endblock %}
diff --git a/fedora-elections/templates/election/results.html b/fedora-elections/templates/election/results.html
new file mode 100644
index 0000000..f61d214
--- /dev/null
+++ b/fedora-elections/templates/election/results.html
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+{% block body %}
+ <h1>{{ election.summary }} Results</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{candidate.name}}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ <td>{{ candidate.vote_count }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+{% endblock %}
diff --git a/fedora-elections/templates/election/vote.html b/fedora-elections/templates/election/vote.html
new file mode 100644
index 0000000..e3c4dd4
--- /dev/null
+++ b/fedora-elections/templates/election/vote.html
@@ -0,0 +1,116 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{election.summary}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
+<form action="" method="post">
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ <a href="{{candidate.url}}">[Info]</a>
+ </td>
+ <!-- If user is logged in: -->
+ {% if g.fas_user %}
+ {% if nextaction == 'vote' %}
+ <td>
+ <select name="{{candidate.id}}">
+ {% for opt in range(0, num_candidates+1) %}
+ <option value={{opt}}>{{opt}}</option>
+ {% endfor %}
+ </select>
+ </td>
+ {% endif %}
+ {% if nextaction == 'confirm' %}
+ <td>
+ {% set cid = candidate.id %}
+ <input type="hidden" name="{{candidate.id}}"
+ value="{{voteinfo[cid]}}" />
+ {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
+ {% if weightpercent >= 90 %}
+ <div class="vthot">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 70 %}
+ <div class="vtwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 50 %}
+ <div class="vtmedwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 30 %}
+ <div class="vtmedcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 10 %}
+ <div class="vtcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% else %}
+ <div class="vtcold">
+ {{voteinfo[cid]}}
+ </div>
+ {% endif %}
+ </td>
+ {% endif %}
+ {% endif %}
+ <!-- End if user logged in -->
+ </tr>
+ {% endfor %}
+ </table>
+ {% if g.fas_user %}
+ <p>
+ {% if nextaction == 'confirm' %}
+ <input type="submit" name="{{nextaction}}" value="Submit" />
+ {% else %}
+ <input type="submit" name="{{nextaction}}" value="Preview" />
+ {% endif %}
+ </p>
+ {% endif %}
+</form>
+
+<!-- Make this conditional on whether the user is logged in and can vote in the election -->
+{% if election.legal_voters.count() != 0 %}
+<p>
+ To vote in this election you must be a member of any one of the following groups:
+</p>
+<ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+</ul>
+{% else %}
+<p />
+{% endif %}
+<div>
+ <h2>Information on Range Voting</h2>
+ <p>
+ Fedora Project has implemented
+ <a href="http://rangevoting.org/">Range Voting</a>
+ for this election, in particular the "Range (score-summing, blanks
+ treated as zero score, no quorum rule)" range voting system.
+ </p>
+ <p>
+ To cast your vote in this election simply select a value between 0 and
+ {{num_candidates}} with 0 as 'least or no preference' and
+ {{num_candidates}} as 'highest preference'.
+ </p>
+ <p>
+ At the end of the election, the highest ranking candidate(s) are marked
+ as the winners.
+ </p>
+ <p>
+ For more information about Range Voting, visit the
+ <a href="http://rangevoting.org/">Center for Range Voting</a>.
+ </p>
+</div>
+{% endblock %}
diff --git a/fedora-elections/templates/list/index.html b/fedora-elections/templates/list/index.html
new file mode 100644
index 0000000..290c2cf
--- /dev/null
+++ b/fedora-elections/templates/list/index.html
@@ -0,0 +1,32 @@
+{% extends "base.html" %}
+{% block body %}
+ {% if num_elections > 0 %}
+ <h2>{{ title }}</h2>
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{ election.summary }}</td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ <td>
+ {% if election.status == "In progress" %}
+ {% if g.fas_user %}
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">
+ Vote now!
+ </a>
+ {% endif %}
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+{% endblock %}
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
deleted file mode 100644
index 86bfdc6..0000000
--- a/fedora_elections/__init__.py
+++ /dev/null
@@ -1,481 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2012 Red Hat, Inc.
-# Copyright © 2012 Ian Weller <ianweller(a)fedoraproject.org>
-# Copyright © 2012 Toshio Kuratomi <tkuratom(a)redhat.com>
-# Copyright © 2012 Frank Chiulli <fchiulli(a)fedoraproject.org>
-#
-# This copyrighted material is made available to anyone wishing to use, modify,
-# copy, or redistribute it subject to the terms and conditions of the GNU
-# General Public License v.2, or (at your option) any later version. This
-# program is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY expressed or implied, including the implied warranties of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
-# Public License for more details. You should have received a copy of the GNU
-# General Public License along with this program; if not, write to the Free
-# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
-# code or documentation are not subject to the GNU General Public License and
-# may only be used or replicated with the express permission of Red Hat, Inc.
-#
-# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
-# Toshio Kuratomi <tkuratom(a)redhat.com>
-# Frank Chiulli <fchiulli(a)fedoraproject.org>
-#
-
-import flask
-from flask.ext.fas import FAS
-from flask.ext.sqlalchemy import SQLAlchemy
-
-from fedora.client import AuthError, AppError
-from fedora.client.fas2 import AccountSystem
-
-from sqlalchemy.orm.exc import NoResultFound
-
-import fedmsgshim
-
-from datetime import datetime, time
-from functools import wraps
-from urlparse import urlparse, urljoin
-
-app = flask.Flask(__name__)
-app.config.from_object('fedora_elections.default_config')
-app.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
-
-# set up SQLAlchemy
-db = SQLAlchemy(app)
-
-# set up FAS
-fas = FAS(app)
-
-# FAS for usernames.
-# baseURL = "https://admin.fedoraproject.org/accounts"
-# fasuser = "elections"
-# faspw = "elections"
-# fas2_url = 'http://fakefas.fedoraproject.org/accounts/'
-fas2_url = app.config['FAS_BASE_URL']
-fas2_username = app.config['FAS_USERNAME']
-fas2_password = app.config['FAS_PASSWORD']
-fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password,
- insecure=True)
-
-
-# modular imports
-from fedora_elections import forms, models, redirect
-
-
-def remove_csrf(form_data):
- return dict([(k, v) for k, v in form_data.items() if k != 'csrf'])
-
-
-def login_required(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- if flask.g.fas_user is None:
- return flask.redirect(flask.url_for('auth_login',
- next=flask.request.url))
- return f(*args, **kwargs)
- return decorated_function
-
-
-def election_admin_required(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- if flask.g.fas_user is None:
- return flask.redirect(flask.url_for('auth_login',
- next=flask.request.url))
- if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
- flask.g.fas_user.groups:
- flask.abort(403)
- return f(*args, **kwargs)
- return decorated_function
-
-
-### LIST VIEWS #############################################
-
-(a)app.route('/')
-def index():
- elections = models.Election.query.filter_by(frontpage=True).all()
- num_elections = len(elections)
- return flask.render_template('list/index.html', elections=elections,
- num_elections=num_elections,
- title="Elections")
-
-
-(a)app.route('/about/<election_alias>')
-def about_election(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template('election/about.html', election=election,
- usernamemap=usernamemap)
-
-
-(a)app.route('/archive')
-def archived_elections():
- now = datetime.utcnow()
-
- try:
- elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date<now, models.Election.id>0).all()
- except NoResultFound:
- flask.flash('There are no archived elections.')
- return redirect.safe_redirect_back()
-
- num_elections = len(elections)
- if (num_elections == 0):
- flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
-
- return flask.render_template('election/archive.html',
- num_elections=num_elections,
- elections=elections)
-
-
-(a)app.route('/open')
-def open_elections():
- now = datetime.utcnow()
-
- try:
- elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date>now,
- models.Election.id>0).all()
- except NoResultFound:
- flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
-
- num_elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date>now,
- models.Election.id>0).count()
- if (num_elections == 0):
- flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
-
- return flask.render_template('list/index.html', elections=elections,
- num_elections=num_elections,
- title='Open Elections')
-
-
-### ELECTION VIEWS #############################################
-
-(a)app.route('/vote/<election_alias>', methods=['GET', 'POST'])
-def vote(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
-
- if election.status == 'Pending':
- flask.flash('Voting has not yet started, sorry.')
- return redirect.safe_redirect_back()
-
- elif election.status == 'Ended':
- flask.flash('This election is closed. You have been redirected to ' +
- 'the election results.')
- return flask.redirect(flask.url_for('results', election.shortname))
-
- votes = models.Vote.query.filter_by(election_id=election.id,
- voter=flask.g.fas_user.username).count()
- if (votes != 0):
- flask.flash('You have already voted in the election!')
- return redirect.safe_redirect_back()
-
- num_candidates = election.candidates.count()
-
- uvotes = {}
- for candidate in election.candidates:
- uvotes[candidate.id] = ""
- next_action = 'vote'
-
- if flask.request.method == 'POST':
- next_action = ''
- form_values = flask.request.form.values()
- if 'Submit' in form_values:
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote >= 0 and vote <= num_candidates:
- uvotes[candidate.id] = int(vote)
- else:
- flask.flash("Invalid Ballot!")
- return redirect.safe_redirect_back()
-
- except ValueError:
- flask.flash("Invalid Ballot!")
- return redirect.safe_redirect_back()
-
- for uvote in uvotes:
- new_vote = models.Vote(election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=uvote,
- value=uvotes[uvote] )
- db.session.add(new_vote)
- db.session.commit()
-
- flask.flash("Your vote has been recorded. Thank you!")
- return redirect.safe_redirect_back()
-
- elif 'Preview' in form_values:
- flask.flash("Please confirm your vote!")
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote > num_candidates:
- flask.flash("Invalid data.")
- uvotes[candidate.id] = 0
- next_action = 'vote'
- elif vote >= 0:
- uvotes[candidate.id] = vote
- else:
- flask.flash("Invalid data2.")
- uvotes[c.id] = 0
- next_action = 'vote'
- except ValueError:
- flask.flash("Invalid data")
-
- if (next_action != 'vote'):
- next_action = 'confirm'
-
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template('election/vote.html', election=election,
- num_candidates=num_candidates,
- usernamemap=usernamemap,
- voteinfo=uvotes, nextaction=next_action)
-
-
-### AUTH VIEWS #############################################
-
-(a)app.route('/login', methods=('GET', 'POST'))
-def auth_login():
- if flask.g.fas_user:
- return redirect.safe_redirect_back()
- form = forms.LoginForm()
- if form.validate_on_submit():
- if fas.login(form.username.data, form.password.data):
- flask.flash('Welcome, %s' % flask.g.fas_user.username)
- return redirect.safe_redirect_back()
- else:
- flask.flash('Incorrect username or password')
- return flask.render_template('auth/login.html', form=form)
-
-
-(a)app.route('/logout')
-def auth_logout():
- if not flask.g.fas_user:
- return redirect.safe_redirect_back()
- fas.logout()
- flask.flash('You have been logged out')
- return redirect.safe_redirect_back()
-
-
-### ELECTION ADMIN VIEWS ###################################
-
-(a)app.route('/admin/')
-@election_admin_required
-def admin_view_elections():
- elections = models.Election.query.\
- filter_by(fas_user=flask.g.fas_user.username).all()
- num_elections = len(elections)
- if (num_elections == 0):
- flask.flash('You do not have any elections.')
- return redirect.safe_redirect_back()
-
- return flask.render_template('admin/all_elections.html',
- num_elections=num_elections,
- elections=elections)
-
-
-(a)app.route('/admin/new', methods=("GET", "POST"))
-@election_admin_required
-def admin_new_election():
- form = forms.ElectionForm()
- if form.validate_on_submit():
- election = models.Election(**remove_csrf(form.data))
- # Fix start_date and end_date to use datetime
- election.start_date = datetime.combine(election.start_date, time())
- election.end_date = datetime.combine(election.end_date,
- time(23, 59, 59))
- election.fas_user = flask.g.fas_user.username
- db.session.add(election)
- admin = models.ElectionAdminGroup(election=election,
- group_name=app.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
- role_required=u'user')
- db.session.add(admin)
- db.session.commit()
- flask.flash('Election "%s" added' % election.summary)
- fedmsgshim.publish(topic="election.new", msg=election)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/election_form.html', form=form,
- submit_text='Create election')
-
-
-(a)app.route('/admin/<election_alias>/')
-@election_admin_required
-def admin_view_election(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
- if not election:
- flask.abort(404)
- return flask.render_template('admin/view_election.html', election=election)
-
-
-(a)app.route('/admin/<election_alias>/edit', methods=("GET", "POST"))
-@election_admin_required
-def admin_edit_election(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
- if not election:
- flask.abort(404)
- form = forms.ElectionForm(election.id, obj=election)
- if form.validate_on_submit():
- form.populate_obj(election)
- db.session.commit()
- flask.flash('Election "%s" saved' % election.summary)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/election_form.html', form=form,
- submit_text='Edit election')
-
-
-(a)app.route('/admin/<election_alias>/candidates/new', methods=("GET", "POST"))
-@election_admin_required
-def admin_add_candidate(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
- if not election:
- flask.abort(404)
- form = forms.CandidateForm()
- if form.validate_on_submit():
- candidate = models.Candidate(election=election,
- **remove_csrf(form.data))
- db.session.add(candidate)
- db.session.commit()
- flask.flash('Candidate "%s" saved' % candidate.name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/candidate.html', form=form,
- submit_text='Add candidate')
-
-
-(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
- methods=("GET", "POST"))
-@election_admin_required
-def admin_edit_candidate(election_alias, candidate_id):
- election = models.Election.query.filter_by(alias=election_alias).first()
- candidate = models.Candidate.query.filter_by(id=candidate_id).first()
- if not election or not candidate:
- flask.abort(404)
- if candidate.election != election:
- flask.abort(404)
- form = forms.CandidateForm(obj=candidate)
- if form.validate_on_submit():
- form.populate_obj(candidate)
- db.session.commit()
- flask.flash('Candidate "%s" saved' % candidate.name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/candidate.html', form=form,
- submit_text='Edit candidate')
-
-
-(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
- methods=("GET", "POST"))
-@election_admin_required
-def admin_delete_candidate(election_alias, candidate_id):
- election = models.Election.query.filter_by(alias=election_alias).first()
- candidate = models.Candidate.query.filter_by(id=candidate_id).first()
- if not election or not candidate:
- flask.abort(404)
- if candidate.election != election:
- flask.abort(404)
- form = forms.ConfirmationForm()
- if form.validate_on_submit():
- db.session.delete(candidate)
- candidate_name = candidate.name
- db.session.commit()
- flask.flash('Candidate "%s" deleted' % candidate_name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/delete_candidate.html', form=form,
- candidate=candidate)
-
-
-(a)app.route('/results/<election_alias>')
-def election_results(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
-
- if election.status == 'In progress':
- flask.flash("We are sorry. The results for this election cannot be" \
- " viewed at this time because the election is still in" \
- " progress.")
- return redirect.safe_redirect_back()
-
- elif election.status == 'Pending':
- flask.flash("We are sorry. The results for this election cannot be" \
- " viewed at this time because the election has not" \
- " started.")
- return redirect.safe_redirect_back()
-
- elif election.embargoed == 1:
- if flask.g.fas_user is None:
- flask.flash("We are sorry. The results for this election" \
- "cannot be viewed because they are currently" \
- " embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
- else:
- if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
- flask.g.fas_user.groups:
- pass
- else:
- match = 0
- admingroups = models.ElectionAdminGroup.query.filter_by(
- election_id=election.id).all()
- for admingroup in admingroups:
- if admingroup.group_name in flask.g.fas_user.groups:
- match = 1
-
- if match == 0:
- flask.flash("We are sorry. The results for this election" \
- "cannot be viewed because they are currently" \
- " embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template('election/results.html', election=election,
- usernamemap=usernamemap)
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
deleted file mode 100644
index 7b5b794..0000000
--- a/fedora_elections/default_config.py
+++ /dev/null
@@ -1 +0,0 @@
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
diff --git a/fedora_elections/fedmsgshim.py b/fedora_elections/fedmsgshim.py
deleted file mode 100644
index 775f785..0000000
--- a/fedora_elections/fedmsgshim.py
+++ /dev/null
@@ -1,17 +0,0 @@
-""" This is a temporary shim that allows fedmsg to be an optional dependency of
-elections. If fedmsg is installed, these function calls will try to actually send
-messages. If it is not installed, it will return silently.
-
- :Author: Ralph Bean <rbean(a)redhat.com>
-
-"""
-
-import warnings
-
-
-def publish(*args, **kwargs):
- try:
- import fedmsg
- fedmsg.publish(*args, **kwargs)
- except Exception, e:
- warnings.warn(str(e))
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
deleted file mode 100644
index f050dbd..0000000
--- a/fedora_elections/forms.py
+++ /dev/null
@@ -1,69 +0,0 @@
-from flask.ext import wtf
-
-from fedora_elections.models import Election
-
-
-class ElectionForm(wtf.Form):
- summary = wtf.TextField('Summary', [wtf.validators.Required(),
- wtf.validators.Length(max=150)])
- alias = wtf.TextField('Alias', [
- wtf.validators.Required(),
- wtf.validators.Length(max=100),
- wtf.validators.Regexp('[a-z0-9_-]+',
- message=('Alias may only contain lower case '
- 'letters, numbers, hyphens and '
- 'underscores.')),
- ])
- description = wtf.TextAreaField('Description', [wtf.validators.Required()])
- url = wtf.TextField('URL', [wtf.validators.Required(),
- wtf.validators.URL(),
- wtf.validators.Length(max=250)])
- start_date = wtf.DateField('Start date', [wtf.validators.Required()])
- end_date = wtf.DateField('End date', [wtf.validators.Required()])
- number_elected = wtf.IntegerField('Number elected',
- [wtf.validators.Required(),
- wtf.validators.NumberRange(min=1)],
- default=1)
- candidates_are_fasusers = wtf.BooleanField('Candidates are FAS users?')
- frontpage = wtf.BooleanField('Show on front page?')
- embargoed = wtf.BooleanField('Embargo results?', default=True)
-
- def __init__(form, election_id=None, *args, **kwargs):
- super(ElectionForm, form).__init__(*args, **kwargs)
- form._election_id = election_id
-
- def validate_summary(form, field):
- check = Election.query.filter_by(summary=form.summary.data).all()
- if check:
- if not (form._election_id and form._election_id == check[0].id):
- raise wtf.ValidationError('There is already another election '
- 'with this summary.')
-
- def validate_alias(form, field):
- if form.alias.data == 'new':
- raise wtf.ValidationError(flask.Markup('The alias cannot be '
- '<code>new</code>.'))
- check = Election.query.filter_by(alias=form.alias.data).all()
- if check:
- if not (form._election_id and form._election_id == check[0].id):
- raise wtf.ValidationError('There is already another election '
- 'with this alias.')
-
- def validate_end_date(form, field):
- if form.end_date.data <= form.start_date.data:
- raise wtf.ValidationError('End date must be later than '
- 'start date.')
-
-
-class CandidateForm(wtf.Form):
- name = wtf.TextField('Name', [wtf.validators.Required()])
- url = wtf.TextField('URL', [wtf.validators.Length(max=250)])
-
-
-class LoginForm(wtf.Form):
- username = wtf.TextField('Username', [wtf.validators.Required()])
- password = wtf.PasswordField('Password', [wtf.validators.Required()])
-
-
-class ConfirmationForm(wtf.Form):
- pass
diff --git a/fedora_elections/images/border-left.png b/fedora_elections/images/border-left.png
deleted file mode 100644
index 6f256df..0000000
Binary files a/fedora_elections/images/border-left.png and /dev/null differ
diff --git a/fedora_elections/images/border-right.png b/fedora_elections/images/border-right.png
deleted file mode 100644
index 0e7365b..0000000
Binary files a/fedora_elections/images/border-right.png and /dev/null differ
diff --git a/fedora_elections/images/fedora-logo.png b/fedora_elections/images/fedora-logo.png
deleted file mode 100644
index ce95893..0000000
Binary files a/fedora_elections/images/fedora-logo.png and /dev/null differ
diff --git a/fedora_elections/images/line.png b/fedora_elections/images/line.png
deleted file mode 100644
index 3498cb9..0000000
Binary files a/fedora_elections/images/line.png and /dev/null differ
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
deleted file mode 100644
index b19bd33..0000000
--- a/fedora_elections/models.py
+++ /dev/null
@@ -1,103 +0,0 @@
-from fedora_elections import app, db
-
-from datetime import datetime
-
-class Election(db.Model):
- __tablename__ = 'elections'
-
- id = db.Column(db.Integer, primary_key=True)
- summary = db.Column(db.Unicode(150), unique=True, nullable=False)
- alias = db.Column(db.Unicode(100), unique=True, nullable=False)
- description = db.Column(db.UnicodeText, nullable=False)
- url = db.Column(db.Unicode(250), nullable=False)
- start_date = db.Column(db.DateTime, nullable=False)
- end_date = db.Column(db.DateTime, nullable=False)
- number_elected = db.Column(db.Integer, nullable=False, default=1)
- embargoed = db.Column(db.Boolean, nullable=False, default=True)
- frontpage = db.Column(db.Boolean, nullable=False, default=False)
- voting_type = db.Column(db.Unicode(100), nullable=False, default=u'range')
- candidates_are_fasusers = db.Column(db.Boolean, nullable=False,
- default=False)
- fas_user = db.Column(db.Unicode(50), nullable=False)
-
- @property
- def status(self):
- if datetime.now() < self.start_date:
- return 'Pending'
- else:
- if datetime.now() < self.end_date:
- return 'In progress'
- else:
- if self.embargoed:
- return 'Embargoed'
- else:
- return 'Ended'
-
- @property
- def locked(self):
- return datetime.now() >= self.start_date
-
-
-class ElectionAdminGroup(db.Model):
- __tablename__ = 'electionadmingroups'
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('admin_groups',
- lazy='dynamic'))
- group_name = db.Column(db.Unicode(150), nullable=False)
- role_required = db.Column(db.Enum(u'user', u'sponsor', u'administrator'),
- nullable=False)
-
-
-class Candidate(db.Model):
- __tablename__ = 'candidates'
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('candidates',
- lazy='dynamic'))
- name = db.Column(db.Unicode(150), nullable=False) # FAS username if
- # candidates_are_fasusers
- url = db.Column(db.Unicode(250))
-
- @property
- def vote_count(self):
- values = [v.value for v in self.votes]
- return sum(values)
-
-
-class LegalVoter(db.Model):
- __tablename__ = 'legalvoters'
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('legal_voters',
- lazy='dynamic'))
- # special names:
- # "cla + one" = cla_done + 1 non-cla group
- group_name = db.Column(db.Unicode(150), nullable=False)
-
-
-class Vote(db.Model):
- __tablename__ = 'votes'
-
- __table_args__ = (db.UniqueConstraint('election_id', 'voter',
- 'candidate_id',
- name='eid_voter_cid'), {} )
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('votes',
- lazy='dynamic'))
- voter = db.Column(db.Unicode(150), nullable=False)
- timestamp = db.Column(db.DateTime, nullable=False)
- candidate_id = db.Column(db.Integer, db.ForeignKey('candidates.id'),
- nullable=False)
- candidate = db.relationship('Candidate', backref=db.backref('votes',
- lazy='dynamic'))
- value = db.Column(db.Integer, nullable=False)
diff --git a/fedora_elections/redirect.py b/fedora_elections/redirect.py
deleted file mode 100644
index 5571abe..0000000
--- a/fedora_elections/redirect.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import flask
-
-from urlparse import urljoin, urlparse
-
-
-def is_safe_url(target):
- ref_url = urlparse(flask.request.host_url)
- test_url = urlparse(urljoin(flask.request.host_url, target))
- return test_url.scheme in ('http', 'https') and \
- ref_url.netloc == test_url.netloc
-
-
-def safe_redirect_back(next=None, fallback=('index', {})):
- targets = []
- if next:
- targets.append(next)
- if 'next' in flask.request.args and \
- flask.request.args['next']:
- targets.append(flask.request.args['next'])
- targets.append(flask.url_for(fallback[0], **fallback[1]))
- for target in targets:
- if is_safe_url(target):
- return flask.redirect(target)
diff --git a/fedora_elections/static/style.css b/fedora_elections/static/style.css
deleted file mode 100644
index 8c4a9b3..0000000
--- a/fedora_elections/static/style.css
+++ /dev/null
@@ -1,144 +0,0 @@
-body {
- font-family: sans-serif;
- font-size: 12pt;
- line-height: 1.35;
-}
-
-#flashes {
- background-color: #ffa;
-}
-
-nav {
- background-color: #eee;
- padding: 0.5em 1em;
-}
-
-nav h2 {
- display: inline;
- font-size: inherit;
- margin-right: 1em;
-}
-
-nav ul {
- display: inline;
- list-style: none;
- padding: 0;
-}
-
-nav li {
- display: inline;
- margin-right: .5em;
-}
-
-.clear {
- clear: both;
-}
-
-/* input elements */
-
-fieldset {
- border: 0;
- padding: 0;
- margin: .5em 0;
- float: left;
- clear: both;
-}
-
-.row {
- margin: .25em 0;
-}
-
-.row label:first-child {
- width: 8em;
- margin-right: 0.75em;
- text-align: right;
- clear: both;
- font-weight: bold;
- display: inline-block;
- vertical-align: top;
-}
-
-.row textarea {
- width: 30em;
- height: 8em;
-}
-
-form .row .errors {
- list-style: none;
- margin: .5em 0 1em 10.9375em;
- padding: 0;
- font-size: .8em;
- color: #c00;
- width: 30em;
-}
-
-div.help {
- width: 40%;
- float: right;
- background-color: #ffa;
- border-top: 1px solid #ee9;
- padding: 1em;
- font-size: .8em;
- margin: .625em 0;
-}
-
-div > *:first-child { margin-top: 0; }
-div > *:last-child { margin-bottom: 0; }
-
-form .buttons.indent {
- margin-left: 8.75em;
-}
-
-.vthot
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #FFFFFF;
- background: #9C0000;
-}
-
-.vtwarm
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #FFA300;
-}
-
-.vtmedwarm
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #93FF63;
-}
-
-.vtmedcool
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #00DAFE;
-}
-
-.vtcool
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #0033FF;
-}
-
-.vtcold
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #FFFFFF;
- background: #000083;
-}
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
deleted file mode 100644
index ff781b7..0000000
--- a/fedora_elections/templates/_formhelpers.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% macro render_field(field, after="") %}
-<div class="row">
- {{ field.label }}{{ field(**kwargs)|safe }}{% if after %} {{ after }}{% endif %}
- {% if field.errors %}
- <ul class="errors">
- {% for error in field.errors %}
- <li>{{ error }}</li>
- {% endfor %}
- </ul>
- {% endif %}
-</div>
-{% endmacro %}
-{# vim: set ts=2 sw=2 sta et: #}
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
deleted file mode 100644
index e168ac0..0000000
--- a/fedora_elections/templates/admin/all_elections.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% extends "base.html" %}
-{% block title %}Election administration{% endblock %}
-{% block body %}
- {% if num_elections > 0 %}
- <h2>Election administration</h2>
- <table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>
- <a href="{{ url_for('admin_view_election',
- election_alias=election.alias) }}">
- {{ election.summary }}
- </a>
- </td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- </tr>
- {% endfor %}
- </table>
- {% endif %}
-{%endblock %}
diff --git a/fedora_elections/templates/admin/candidate.html b/fedora_elections/templates/admin/candidate.html
deleted file mode 100644
index a7b2fda..0000000
--- a/fedora_elections/templates/admin/candidate.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}{{ submit_text }}{% endblock %}
-{% block body %}
- <h2>{{ submit_text }}</h2>
-
- <form action="" method="post">
- <fieldset>
- {{ render_field(form.name) }}
- {{ render_field(form.url) }}
- </fieldset>
-
- <br class="clear">
- <p class="buttons">
- <input type="submit" class="submit positive" value="{{ submit_text }}">
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora_elections/templates/admin/delete_candidate.html b/fedora_elections/templates/admin/delete_candidate.html
deleted file mode 100644
index d764f19..0000000
--- a/fedora_elections/templates/admin/delete_candidate.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}Delete candidate{% endblock %}
-{% block body %}
- <h2>{{ self.title() }}</h2>
-
- <form action="" method="post">
- <p>Are you sure you want to delete candidate "{{ candidate.name }}"?</p>
- <p class="buttons">
- <input type="submit" value="Delete candidate">
- <a href="{{ url_for('admin_view_election',
- election_alias=candidate.election.alias) }}">
- Cancel
- </a>
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
deleted file mode 100644
index 6e2a3d6..0000000
--- a/fedora_elections/templates/admin/election_form.html
+++ /dev/null
@@ -1,77 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}{{ submit_text }}{% endblock %}
-{% block body %}
- <h2>{{ self.title() }}</h2>
-
- <form action="" method="post">
- <fieldset>
- {{ render_field(form.summary) }}
- </fieldset>
- <div class="help">
- <p>This is the name of the election that users will first see.</p>
- <p>Example: <kbd>Fedora Board, November 2010</kbd></p>
- </div>
- <fieldset>
- {{ render_field(form.alias) }}
- </fieldset>
- <div class="help">
- <p>
- A short, unique name that will be used for the election's URL.
- Lowercase letters, numbers, hyphens and underscores only.
- </p>
- <p>Example: <kbd>boardf15</kbd></p>
- </div>
- <fieldset>
- {{ render_field(form.description) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.url) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.start_date, " at 00:00:00 UTC") }}
- {{ render_field(form.end_date, " at 23:59:59 UTC") }}
- </fieldset>
- <div class="help">
- <p>Enter dates in <kbd>YYYY-MM-DD</kbd> format.</p>
- </div>
- <fieldset>
- {{ render_field(form.number_elected) }}
- </fieldset>
- <div class="help">
- <p>
- This determines the cutoff for how many candidates win the election.
- For example, if you are electing four empty seats on a committee,
- enter <kbd>4</kbd> here.
- </p>
- </div>
- <fieldset>
- {{ render_field(form.candidates_are_fasusers) }}
- </fieldset>
- <div class="help">
- <p>
- Check this if every candidate in this election has a FAS user account.
- This will automatically pull full names and wiki page URLs from FAS.
- </p>
- </div>
- <fieldset>
- {{ render_field(form.frontpage) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.embargoed) }}
- </fieldset>
- <div class="help">
- <p>
- Embargo results when the election is finished. To unembargo them, edit
- the election and uncheck this box.
- </p>
- </div>
-
- <br class="clear">
- {% block election_form_extra %}{% endblock %}
- <p class="buttons">
- <input type="submit" class="submit positive" value="Save Changes">
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
deleted file mode 100644
index ed070f3..0000000
--- a/fedora_elections/templates/admin/view_election.html
+++ /dev/null
@@ -1,42 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- <h2>{{ election.summary }}</h2>
- <p>{{ election.description }}</p>
- <p>
- <a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">
- Edit election
- </a>
- </p>
- <ul>
- <li>On front page: {{ election.frontpage }}</li>
- <li>Start date: {{ election.start_date }}</li>
- <li>End date: {{ election.end_date }}</li>
- <li>Number elected: {{ election.number_elected }}</li>
- <li>Election type: {{ election.voting_type }}</li>
- </ul>
- <h3>Candidates</h3>
- {% if election.candidates.all() %}
- <ul>
- {% for candidate in election.candidates.all() %}
- <li>{{ candidate.name }}
- <a href="{{ url_for('admin_edit_candidate',
- election_alias=election.alias,
- candidate_id=candidate.id) }}">
- [edit]
- </a>
- <a href="{{ url_for('admin_delete_candidate',
- election_alias=election.alias,
- candidate_id=candidate.id) }}">
- [x]
- </a>
- </li>
- {% endfor %}
- </ul>
- {% else %}
- <p>There are no candidates.</p>
- {% endif %}
- <a href="{{ url_for('admin_add_candidate',
- election_alias=election.alias) }}">
- Add a candidate
- </a>
-{% endblock %}
diff --git a/fedora_elections/templates/auth/login.html b/fedora_elections/templates/auth/login.html
deleted file mode 100644
index 9e7b73c..0000000
--- a/fedora_elections/templates/auth/login.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}Log in{% endblock %}
-{% block body %}
- <h2>Log in</h2>
-
- <form action="" method="post">
- <p>
- Log in using your
- <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a>
- account.
- </p>
- {{ render_field(form.username) }}
- {{ render_field(form.password) }}
- <p class="buttons indent"><input type="submit" class="submit positive"
- value="Log in">
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
deleted file mode 100644
index 3e6f806..0000000
--- a/fedora_elections/templates/base.html
+++ /dev/null
@@ -1,134 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- {% block head %}
- <title>
- Fedora Elections
- {% if self.title() %}
- —
- {% block title %}{% endblock %}
- {% endif %}
- </title>
- <link rel="stylesheet" type="text/css" media="screen"
- href="{{ url_for("static", filename="style.css") }}" />
- <link rel="stylesheet" type="text/css" media="screen"
- href="https://fedoraproject.org/static/css/fedora.css" />
- {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
- {% endfor %}
- {% endblock %}
- </head>
- <body>
- <div id="wrapper">
- <div id="head">
- <h1><a href="https://admin.fedoraproject.org/en/index">Fedora</a></h1>
- </div>
-
- <div id="sidebar">
- <div id="nav">
- <h2>Navigation</h2>
- <ul>
- <li id="nav-home">
- <a href="http://fedoraproject.org/en/index">Home</a>
- </li>
- <li id="nav-get">
- <a href="http://fedoraproject.org/en/get-fedora">Get Fedora</a>
- </li>
- <li id="nav-join">
- <a href="http://fedoraproject.org/en/join-fedora">Join Fedora</a>
- </li>
- <li id="nav-help">
- <a href="http://fedoraproject.org/en/get-help">Get Help</a>
- </li>
- </ul>
- </div>
- </div>
-
- <div id="content">
- <ul class="toolbar" id="#main-toolbar">
- {% if g.fas_user %}
- <li>
- Hello, {{ g.fas_user.username }} —
- <a href="{{ url_for("auth_logout") }}">log out</a>
- </li>
- {% else %}
- <li>
- You are not logged in —
- <a href="{{ url_for("auth_login") }}">log in</a>
- </li>
- {% endif %}
- </ul>
- <h2>Elections</h2>
- <ul>
- <li>
- <a href="{{ url_for("open_elections") }}">
- Open elections
- </a>
- </li>
- <li>
- <a href="{{ url_for("archived_elections") }}">
- Archived elections
- </a>
- </li>
- </ul>
- <br>
- <h2>Administration</h2>
- <ul>
- <li>
- <a href="{{ url_for("admin_view_elections") }}">
- My elections
- </a>
- </li>
- <li>
- <a href="{{ url_for("admin_new_election") }}">
- Create new election
- </a>
- </li>
- </ul>
- {% with messages = get_flashed_messages() %}
- {% if messages %}
- <ul id="flashes">
- {% for message in messages %}
- <li>{{ message }}</li>
- {% endfor %}
- </ul>
- {% endif %}
- {% endwith %}
- {% block body %}
- {% endblock %}
- </div>
- </div>
- <div id="bottom">
- <div id="footer">
- {% block footer %}
- <p class="copy">
- Copyright © 2008 Red Hat, Inc. and others. Please send any
- comments or corrections to the
- <a href="mailto:webmaster@fedoraproject.org">
- websites team
- </a>.
- </p>
- <p class="disclaimer">
- The Fedora Project is maintained and driven by the community and
- sponsored by Red Hat. This is a community maintained site. Red
- Hat is not responsible for content.
- </p>
- <ul>
- <li class="first">
- <a href="/en/sponsors">Sponsors</a>
- </li>
- <li>
- <a href="http://fedoraproject.org/wiki/Legal">
- Legal
- </a>
- </li>
- <li>
- <a href="http://fedoraproject.org/wiki/Legal/TrademarkGuidelines">
- Trademark Guidelines
- </a>
- </li>
- </ul>
- </div>
- {% endblock %}
- </div>
- </body>
-</html>
diff --git a/fedora_elections/templates/election/about.html b/fedora_elections/templates/election/about.html
deleted file mode 100644
index c7bf0ec..0000000
--- a/fedora_elections/templates/election/about.html
+++ /dev/null
@@ -1,40 +0,0 @@
-{% extends "base.html" %}
-{% block title %}Election Information{% endblock %}
-{% block body %}
- <h1>{{ election.summary }}</h1>
- <p>{{ election.description }}</p>
- {% if election.url %}
- <p><a href="{{ election.url }}">[More Information]</a></p>
- {% endif %}
- {% if election.status == 'Ended' %}
- <a href="/results/{{ election.alias }}">Show Results!</a>
- {% endif %}
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- {% if candidate.url %}
- <small><a href="{{ candidate.url }}">[info]</a></small>
- {% endif %}
- </td>
- </tr>
- {% endfor %}
- </table>
- {% if election.legal_voters.count() != 0 %}
- <p>
- To vote in this election you must be a member of any one of the following groups:
- </p>
- <ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
- </ul>
- {% else %}
- <p />
- {% endif %}
-{% endblock %}
diff --git a/fedora_elections/templates/election/archive.html b/fedora_elections/templates/election/archive.html
deleted file mode 100644
index 8740b3c..0000000
--- a/fedora_elections/templates/election/archive.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- {% if num_elections > 0 %}
- <h2>Past Elections</h2>
- <table border="1" cellpadding="1">
- <tr>
- <th></th>
- <th>Start Date (UTC)</th>
- <th>End Date (UTC)</th>
- <th></th>
- <th></th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>{{election.description}}</td>
- <td>{{election.start_date}}</td>
- <td>{{election.end_date}}</td>
- <td>
- <a href="about/{{ election.alias }}"">More information</a>
- </td>
- <td>
- <a href="results/{{ election.alias }}">Results</a>
- </td>
- </tr>
- {% endfor %}
- </table>
- {% endif %}
-{% endblock %}
diff --git a/fedora_elections/templates/election/results.html b/fedora_elections/templates/election/results.html
deleted file mode 100644
index f61d214..0000000
--- a/fedora_elections/templates/election/results.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- <h1>{{ election.summary }} Results</h1>
- <p>{{ election.description }}</p>
- {% if election.url %}
- <p><a href="{{ election.url }}">[More Information]</a></p>
- {% endif %}
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{candidate.name}}
- {% endif %}
- {% if candidate.url %}
- <small><a href="{{ candidate.url }}">[info]</a></small>
- {% endif %}
- </td>
- <td>{{ candidate.vote_count }}</td>
- </tr>
- {% endfor %}
- </table>
-{% endblock %}
diff --git a/fedora_elections/templates/election/vote.html b/fedora_elections/templates/election/vote.html
deleted file mode 100644
index e3c4dd4..0000000
--- a/fedora_elections/templates/election/vote.html
+++ /dev/null
@@ -1,116 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-<h2>{{election.summary}}</h2>
-
-<p>{{election.description}}</p>
-
-<p><a href="{{election.url}}">[More Information]</a></p>
-
-<form action="" method="post">
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- <a href="{{candidate.url}}">[Info]</a>
- </td>
- <!-- If user is logged in: -->
- {% if g.fas_user %}
- {% if nextaction == 'vote' %}
- <td>
- <select name="{{candidate.id}}">
- {% for opt in range(0, num_candidates+1) %}
- <option value={{opt}}>{{opt}}</option>
- {% endfor %}
- </select>
- </td>
- {% endif %}
- {% if nextaction == 'confirm' %}
- <td>
- {% set cid = candidate.id %}
- <input type="hidden" name="{{candidate.id}}"
- value="{{voteinfo[cid]}}" />
- {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
- {% if weightpercent >= 90 %}
- <div class="vthot">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 70 %}
- <div class="vtwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 50 %}
- <div class="vtmedwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 30 %}
- <div class="vtmedcool">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 10 %}
- <div class="vtcool">
- {{voteinfo[cid]}}
- </div>
- {% else %}
- <div class="vtcold">
- {{voteinfo[cid]}}
- </div>
- {% endif %}
- </td>
- {% endif %}
- {% endif %}
- <!-- End if user logged in -->
- </tr>
- {% endfor %}
- </table>
- {% if g.fas_user %}
- <p>
- {% if nextaction == 'confirm' %}
- <input type="submit" name="{{nextaction}}" value="Submit" />
- {% else %}
- <input type="submit" name="{{nextaction}}" value="Preview" />
- {% endif %}
- </p>
- {% endif %}
-</form>
-
-<!-- Make this conditional on whether the user is logged in and can vote in the election -->
-{% if election.legal_voters.count() != 0 %}
-<p>
- To vote in this election you must be a member of any one of the following groups:
-</p>
-<ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
-</ul>
-{% else %}
-<p />
-{% endif %}
-<div>
- <h2>Information on Range Voting</h2>
- <p>
- Fedora Project has implemented
- <a href="http://rangevoting.org/">Range Voting</a>
- for this election, in particular the "Range (score-summing, blanks
- treated as zero score, no quorum rule)" range voting system.
- </p>
- <p>
- To cast your vote in this election simply select a value between 0 and
- {{num_candidates}} with 0 as 'least or no preference' and
- {{num_candidates}} as 'highest preference'.
- </p>
- <p>
- At the end of the election, the highest ranking candidate(s) are marked
- as the winners.
- </p>
- <p>
- For more information about Range Voting, visit the
- <a href="http://rangevoting.org/">Center for Range Voting</a>.
- </p>
-</div>
-{% endblock %}
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
deleted file mode 100644
index 290c2cf..0000000
--- a/fedora_elections/templates/list/index.html
+++ /dev/null
@@ -1,32 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- {% if num_elections > 0 %}
- <h2>{{ title }}</h2>
- <table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- <th></th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>{{ election.summary }}</td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- <td>
- {% if election.status == "In progress" %}
- {% if g.fas_user %}
- <a href="{{ url_for('vote', election_alias=election.alias) }}">
- Vote now!
- </a>
- {% endif %}
- {% endif %}
- </td>
- </tr>
- {% endfor %}
- </table>
- {% endif %}
-{% endblock %}
diff --git a/httpd-fedora-elections.conf.in b/httpd-fedora-elections.conf.in
deleted file mode 100644
index c974b88..0000000
--- a/httpd-fedora-elections.conf.in
+++ /dev/null
@@ -1,23 +0,0 @@
-Alias /fedora-elections/images @DATADIR@/fedora-elections/images
-Alias /fedora-elections/static @DATADIR@/fedora-elections/static
-
-WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
-WSGISocketPrefix run/wsgi
-WSGIRestrictStdout On
-WSGIRestrictSignal Off
-WSGIPythonOptimize 1
-
-WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora-elections/fedora-elections.wsgi
-
-<Location /fedora-elections>
- WSGIProcessGroup fedora-elections
- <IfModule mod_authz_core.c>
- # Apache 2.4
- Require all granted
- </IfModule>
- <IfModule !mod_authz_core.c>
- # Apache 2.2
- Order deny,allow
- Allow from all
- </IfModule>
-</Location>
commit 2c35023daa442f40fd70477304fcfed6b5324a48
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Aug 24 03:33:26 2013 +0000
Update instructions
diff --git a/README b/README
index 610e7d6..442ebb5 100644
--- a/README
+++ b/README
@@ -50,9 +50,11 @@ You can obtain the code from github:
Configure the application
-------------------------
-./configure.py --install-conf=`pwd` --install-data=`pwd` --install-sbin=`pwd`
+cd fedora-elections
+../configure.py --install-conf=${PWD%/*} --install-data=${PWD%/*} --shared-state=${PWD%/*}
-This will create fedora-elections.wsgi and http-fedora-elections.conf.
+This will create createdb, fedora-elections.cfg, fedora-elections.wsgi and
+http-fedora-elections.conf.
Modify fedora-elections.cfg to change FAS_PASSWORD to the correct password.
commit c20c7ac0bbd25566fc3d01f77a9c8a71703af627
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Thu Jul 25 03:20:53 2013 +0000
Add constraint to votes table
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index dde8951..b19bd33 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -85,6 +85,10 @@ class LegalVoter(db.Model):
class Vote(db.Model):
__tablename__ = 'votes'
+ __table_args__ = (db.UniqueConstraint('election_id', 'voter',
+ 'candidate_id',
+ name='eid_voter_cid'), {} )
+
id = db.Column(db.Integer, primary_key=True)
election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
nullable=False)
commit db842723b9b480acc63a74a243832333becd37e0
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Thu Jul 25 03:20:23 2013 +0000
Fix processing 'My Elections'
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 83217bf..86bfdc6 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -444,21 +444,28 @@ def election_results(election_alias):
return redirect.safe_redirect_back()
elif election.embargoed == 1:
- if identity.in_group(config.get('admingroup', 'elections')):
- pass
- else :
- match = 0
- admingroups = models.ElectionAdminGroups.query.filter_by(
- election_id=election.id).all()
- for group in admingroups:
- if identity.in_group(group.group_name):
- match = 1
-
- if match == 0:
+ if flask.g.fas_user is None:
flask.flash("We are sorry. The results for this election" \
"cannot be viewed because they are currently" \
" embargoed pending formal announcement.")
return redirect.safe_redirect_back()
+ else:
+ if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
+ flask.g.fas_user.groups:
+ pass
+ else:
+ match = 0
+ admingroups = models.ElectionAdminGroup.query.filter_by(
+ election_id=election.id).all()
+ for admingroup in admingroups:
+ if admingroup.group_name in flask.g.fas_user.groups:
+ match = 1
+
+ if match == 0:
+ flask.flash("We are sorry. The results for this election" \
+ "cannot be viewed because they are currently" \
+ " embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
usernamemap = {}
if (election.candidates_are_fasusers):
commit eb0da125e43b81887abbd36507462897a7c3c4e6
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jul 6 03:14:04 2013 +0000
Add task
diff --git a/TODO b/TODO
index ebace48..9573c62 100644
--- a/TODO
+++ b/TODO
@@ -6,6 +6,7 @@ Things that need doing to get this deployed
* rpmify
* rpmify flask-fas (or get flask-fas into python-fedora)
* Implement a voting method for range voting
+* Implement a voting method for non-range voting
* CSRF protection -- anything going through wtf forms is protected. It's up to
us to make sure that no data changing requests are coded without wtf forms.
commit 75f057138f95d253622263eecb4e42ac534e27ea
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jul 6 03:13:25 2013 +0000
Fix db issues
diff --git a/fedora-elections.spec b/fedora-elections.spec
index a130b53..61a39ac 100644
--- a/fedora-elections.spec
+++ b/fedora-elections.spec
@@ -127,7 +127,8 @@ rm -rf $RPM_BUILD_ROOT
%defattr(-,root,root,-)
%doc
%dir %{_sysconfdir}/%{name}
-%dir %{_sharedstatedir}/%{name}/
+%dir %{_sharedstatedir}/%{name}
+%attr(744,apache,apache) %{_sharedstatedir}/%{name}
%{_datadir}/%{name}/
%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
%config(noreplace) %{_sysconfdir}/httpd/conf.d/httpd-%{name}.conf
@@ -144,6 +145,10 @@ then
#
echo " Creating a database."
%{_datadir}/%{name}/createdb
+ echo " Changing permissions."
+ chmod 644 database.sqlite
+ echo " Changing owner."
+ chown apache:apache database.sqlite
fi
commit ddb4d65b9f393d9739a09594701f7be160c926d6
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jul 6 03:12:38 2013 +0000
Specify dummy FAS account
diff --git a/fedora-elections.cfg.in b/fedora-elections.cfg.in
index fef1c4a..ae68f66 100644
--- a/fedora-elections.cfg.in
+++ b/fedora-elections.cfg.in
@@ -8,8 +8,8 @@ SECRET_KEY = 'change me'
SQLALCHEMY_DATABASE_URI = 'sqlite:///@SHAREDSTDIR@/fedora-elections/database.sqlite'
FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
-FAS_USERNAME = 'elections'
-FAS_PASSWORD = 'elections_fas_password'
+FAS_USERNAME = 'fcelections'
+FAS_PASSWORD = 'el3cTo$hozer'
FAS_CHECK_CERT = False
FAS_HTTPS_REQUIRED = False
commit cba79d35f218d74dc45370e5138ae3c75e4bdd02
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jul 6 03:11:26 2013 +0000
Add elections owner to db
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 18cf36f..83217bf 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -135,7 +135,14 @@ def archived_elections():
flask.flash('There are no archived elections.')
return redirect.safe_redirect_back()
- return flask.render_template('election/archive.html', elections=elections)
+ num_elections = len(elections)
+ if (num_elections == 0):
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('election/archive.html',
+ num_elections=num_elections,
+ elections=elections)
@app.route('/open')
@@ -158,6 +165,7 @@ def open_elections():
return redirect.safe_redirect_back()
return flask.render_template('list/index.html', elections=elections,
+ num_elections=num_elections,
title='Open Elections')
@@ -290,8 +298,15 @@ def auth_logout():
@app.route('/admin/')
@election_admin_required
def admin_view_elections():
- elections = models.Election.query.filter_by().all()
+ elections = models.Election.query.\
+ filter_by(fas_user=flask.g.fas_user.username).all()
+ num_elections = len(elections)
+ if (num_elections == 0):
+ flask.flash('You do not have any elections.')
+ return redirect.safe_redirect_back()
+
return flask.render_template('admin/all_elections.html',
+ num_elections=num_elections,
elections=elections)
@@ -305,6 +320,7 @@ def admin_new_election():
election.start_date = datetime.combine(election.start_date, time())
election.end_date = datetime.combine(election.end_date,
time(23, 59, 59))
+ election.fas_user = flask.g.fas_user.username
db.session.add(election)
admin = models.ElectionAdminGroup(election=election,
group_name=app.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 994f144..dde8951 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -18,6 +18,7 @@ class Election(db.Model):
voting_type = db.Column(db.Unicode(100), nullable=False, default=u'range')
candidates_are_fasusers = db.Column(db.Boolean, nullable=False,
default=False)
+ fas_user = db.Column(db.Unicode(50), nullable=False)
@property
def status(self):
commit f1ae6366a0fb99c9b3b22b31371b667abfcb6e4b
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jul 6 03:10:44 2013 +0000
Fix no elections issue
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index fafefee..e168ac0 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -1,26 +1,28 @@
{% extends "base.html" %}
{% block title %}Election administration{% endblock %}
{% block body %}
- <h2>Election administration</h2>
- <table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- </tr>
- {% for election in elections %}
+ {% if num_elections > 0 %}
+ <h2>Election administration</h2>
+ <table>
<tr>
- <td>
- <a href="{{ url_for('admin_view_election',
- election_alias=election.alias) }}">
- {{ election.summary }}
- </a>
- </td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
</tr>
- {% endfor %}
- </table>
+ {% for election in elections %}
+ <tr>
+ <td>
+ <a href="{{ url_for('admin_view_election',
+ election_alias=election.alias) }}">
+ {{ election.summary }}
+ </a>
+ </td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
{%endblock %}
diff --git a/fedora_elections/templates/election/archive.html b/fedora_elections/templates/election/archive.html
index ad35d6b..8740b3c 100644
--- a/fedora_elections/templates/election/archive.html
+++ b/fedora_elections/templates/election/archive.html
@@ -1,28 +1,28 @@
{% extends "base.html" %}
{% block body %}
-
- <h2>Past Elections</h2>
-
- <table border="1" cellpadding="1">
- <tr>
- <th></th>
- <th>Start Date (UTC)</th>
- <th>End Date (UTC)</th>
- <th></th>
- <th></th>
- </tr>
- {% for election in elections %}
+ {% if num_elections > 0 %}
+ <h2>Past Elections</h2>
+ <table border="1" cellpadding="1">
<tr>
- <td>{{election.description}}</td>
- <td>{{election.start_date}}</td>
- <td>{{election.end_date}}</td>
- <td>
- <a href="about/{{ election.alias }}"">More information</a>
- </td>
- <td>
- <a href="results/{{ election.alias }}">Results</a>
- </td>
+ <th></th>
+ <th>Start Date (UTC)</th>
+ <th>End Date (UTC)</th>
+ <th></th>
+ <th></th>
</tr>
- {% endfor %}
- </table>
+ {% for election in elections %}
+ <tr>
+ <td>{{election.description}}</td>
+ <td>{{election.start_date}}</td>
+ <td>{{election.end_date}}</td>
+ <td>
+ <a href="about/{{ election.alias }}"">More information</a>
+ </td>
+ <td>
+ <a href="results/{{ election.alias }}">Results</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
{% endblock %}
commit 2a2e646f66e905de6cfd89185b96a52cc6a8f403
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jun 15 03:56:56 2013 +0000
Changes to support building a good package
diff --git a/.gitignore b/.gitignore
index 7ecf5f6..ce498f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@ database.sqlite
*.pyc
*.pyo
*.swp
+createdb
development.cfg
fedora-elections.cfg
fedora-elections.wsgi
diff --git a/README b/README
index 69d11ea..610e7d6 100644
--- a/README
+++ b/README
@@ -54,6 +54,7 @@ Configure the application
This will create fedora-elections.wsgi and http-fedora-elections.conf.
+Modify fedora-elections.cfg to change FAS_PASSWORD to the correct password.
Create a database
-----------------
diff --git a/configure.py b/configure.py
index 9ea3f51..66d899f 100755
--- a/configure.py
+++ b/configure.py
@@ -35,23 +35,23 @@ parser.add_option('--install-conf', dest='confdir', action='store',
parser.add_option('--install-data', dest='datadir', action='store',
default='/usr/share',
help='Where to install data files.')
-parser.add_option('--install-sbindir', dest='sbindir', action='store',
- default='/usr/sbin',
- help='Where to install executeable files.')
+parser.add_option('--shared-state', dest='sharedstdir', action='store',
+ default='/var/lib',
+ help='Where to install shared state files.')
(options, args) = parser.parse_args()
-confdir = options.confdir
-datadir = options.datadir
-sbindir = options.sbindir
+confdir = options.confdir
+datadir = options.datadir
+sharedstdir = options.sharedstdir
-print " confdir = %s" % confdir
-print " datadir = %s" % datadir
-print " sbindir = %s" % sbindir
+print " confdir = %s" % confdir
+print " datadir = %s" % datadir
+print " sharedstdir = %s" % sharedstdir
substs = {'@CONFDIR@':confdir,
'@DATADIR@':datadir,
- '@SBINDIR@':sbindir
+ '@SHAREDSTDIR@':sharedstdir
}
pattern = re.compile("\.in$")
diff --git a/createdb b/createdb
index 4699a56..115635b 100755
--- a/createdb
+++ b/createdb
@@ -1 +1 @@
-FEDORA_ELECTIONS_CONFIG=../development.cfg python createdb.py
+FEDORA_ELECTIONS_CONFIG=/etc/fedora-elections/fedora-elections.cfg python createdb.py
diff --git a/createdb.in b/createdb.in
new file mode 100644
index 0000000..c7a87da
--- /dev/null
+++ b/createdb.in
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora-elections/fedora-elections.cfg python @DATADIR(a)/fedora-elections/createdb.py
diff --git a/createdb.py b/createdb.py
index e99a35c..a909c5e 100644
--- a/createdb.py
+++ b/createdb.py
@@ -1,2 +1,6 @@
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7']
+import pkg_resources
+
from fedora_elections import db
db.create_all()
diff --git a/fedora-elections.cfg.in b/fedora-elections.cfg.in
index fc0d50d..fef1c4a 100644
--- a/fedora-elections.cfg.in
+++ b/fedora-elections.cfg.in
@@ -5,11 +5,11 @@ DEBUG = True
# You will want to change this for your install
SECRET_KEY = 'change me'
-SQLALCHEMY_DATABASE_URI = 'sqlite:///@DATADIR@/database.sqlite'
+SQLALCHEMY_DATABASE_URI = 'sqlite:///@SHAREDSTDIR@/fedora-elections/database.sqlite'
-FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
+FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
FAS_USERNAME = 'elections'
-FAS_PASSWORD ='el3ctMEn0w'
+FAS_PASSWORD = 'elections_fas_password'
FAS_CHECK_CERT = False
FAS_HTTPS_REQUIRED = False
diff --git a/fedora-elections.spec b/fedora-elections.spec
index 7eff44d..a130b53 100644
--- a/fedora-elections.spec
+++ b/fedora-elections.spec
@@ -1,3 +1,8 @@
+%{!?pyver: %define pyver %(%{__python} -c "import sys ; print sys.version[:3]")}
+
+%define modname fedora_elections
+%define eggname fedora_elections
+
Name: fedora-elections
Version: 0.1
Release: 1%{?dist}
@@ -75,20 +80,43 @@ CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
%install
rm -rf $RPM_BUILD_ROOT
-%{__python} setup.py install -O1 --skip-build --install-data=%{_datadir} --root $RPM_BUILD_ROOT
+%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT
%{__mkdir_p} %{buildroot}%{_sysconfdir}/%{name}
-%{__mkdir_p} %{buildroot}%{_sysconfdir}/httpd/conf.d
-%{__mkdir_p} %{buildroot}%{_datadir}/%{name}
-%{__mkdir_p} %{buildroot}%{_datadir}/%{name}/static
+%{__install} -m 444 %{name}.cfg %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
-%{__install} -m 644 httpd-fedora-elections.conf \
+%{__mkdir_p} %{buildroot}%{_sysconfdir}/httpd/conf.d
+%{__install} -m 444 httpd-fedora-elections.conf \
%{buildroot}%{_sysconfdir}/httpd/conf.d/httpd-fedora-elections.conf
-%{__install} -m 644 fedora_elections/static/* %{buildroot}%{_datadir}/%{name}/static
-%{__install} -m 644 %{name}.cfg.sample %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
-%{__install} -m 644 createdb.py %{buildroot}%{_datadir}/%{name}/createdb.py
-%{__install} -m 644 %{name}.wsgi %{buildroot}%{_datadir}/%{name}/%{name}.wsgi
+%{__mkdir_p} %{buildroot}%{_datadir}/%{name}
+%{__install} -m 544 createdb %{buildroot}%{_datadir}/%{name}/createdb
+%{__install} -m 544 createdb.py %{buildroot}%{_datadir}/%{name}/createdb.py
+%{__install} -m 444 %{name}.wsgi %{buildroot}%{_datadir}/%{name}/%{name}.wsgi
+
+%{__mkdir_p} %{buildroot}%{_datadir}/%{name}/static
+%{__install} -m 444 fedora_elections/static/* %{buildroot}%{_datadir}/%{name}/static
+
+%{__mkdir_p} %{buildroot}%{_datadir}/%{name}/images
+%{__install} -m 444 fedora_elections/images/* %{buildroot}%{_datadir}/%{name}/images
+
+%{__mkdir_p} %{buildroot}%{_sharedstatedir}/%{name}
+
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/admin
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/auth
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/election
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/list
+%{__install} -m 444 fedora_elections/templates/*.html \
+ %{buildroot}%{python_sitelib}/%{modname}/templates
+%{__install} -m 444 fedora_elections/templates/admin/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/admin
+%{__install} -m 444 fedora_elections/templates/auth/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/auth
+%{__install} -m 444 fedora_elections/templates/election/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/election
+%{__install} -m 444 fedora_elections/templates/list/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/list
%clean
@@ -99,10 +127,42 @@ rm -rf $RPM_BUILD_ROOT
%defattr(-,root,root,-)
%doc
%dir %{_sysconfdir}/%{name}
+%dir %{_sharedstatedir}/%{name}/
+%{_datadir}/%{name}/
%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
%config(noreplace) %{_sysconfdir}/httpd/conf.d/httpd-%{name}.conf
-%{_datadir}/%{name}
-%{python_sitelib}/*
+%{python_sitelib}/%{modname}/
+%{python_sitelib}/%{eggname}-%{version}-py%{pyver}.egg-info/
+
+
+%post
+cd %{_sharedstatedir}/%{name}
+if [[ ! -f database.sqlite ]]
+then
+ #
+ # The database does not exist create one.
+ #
+ echo " Creating a database."
+ %{_datadir}/%{name}/createdb
+fi
+
+
+%postun
+cd %{_sharedstatedir}/%{name}
+if [[ -f database.sqlite ]]
+then
+ #
+ # Remove the database.
+ #
+ echo " Removing the database."
+ /bin/rm database.sqlite
+fi
+
+cd ${_sharedstatedir}
+if [[ -d %{_sharedstatedir}/%{name} ]]
+then
+ /bin/rmdir %{_sharedstatedir}/%{name}
+fi
%changelog
diff --git a/fedora-elections.wsgi.in b/fedora-elections.wsgi.in
index c7cfbb6..430cc5c 100644
--- a/fedora-elections.wsgi.in
+++ b/fedora-elections.wsgi.in
@@ -11,12 +11,11 @@ import sys
sys.stdout = sys.stderr
## Set the environment variable pointing to the configuration file
-os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections.cfg'
+os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
## The following is only needed if you did not install
## as a python module (for example if you run it from a git clone).
import sys
# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
-sys.path.insert(0, '@SBINDIR@')
from fedora_elections import app as application
diff --git a/fedora_elections/static/fedora.css b/fedora_elections/static/fedora.css
deleted file mode 100644
index e56f85f..0000000
--- a/fedora_elections/static/fedora.css
+++ /dev/null
@@ -1,1334 +0,0 @@
-*
-{
- margin: 0;
- padding: 0;
-}
-
-html, body
-{
- height: 100%;
-}
-
-body
-{
- font-size: 76%;
- background: #FFFFFF url(../images/border-left.png) 0 0 repeat-y;
-}
-
-a img
-{
- border: none;
-}
-
-pre
-{
- font-size: 2.6ex;
- overflow: auto;
-}
-
-abbr, acronym {
- border-bottom: 1px dotted #000000;
- cursor: help;
-}
-
-#wrapper
-{
- font: normal 1em/1.5 sans-serif;
- margin-left: 18px;
- min-height: 100%;
- background: #FFFFFF url(../images/border-right.png) 100% 0 repeat-y;
- padding-right: 18px;
- overflow: hidden;
-}
-
-#head
-{
- position: relative;
- border-top: 10px solid #337ACC;
- padding: 1ex 2ex 17px;
- background: #FFFFFF url(../images/line.png) 0 100% repeat-x;
-}
-
-#head h1 a
-{
- display: block;
- text-indent: -9999px;
- background: url(../images/fedora-logo.png) 20px 50% no-repeat;
- height: 73px;
- width: 175px;
- overflow: hidden;
- text-decoration: none;
-}
-
-#sidebar
-{
- float: left;
- width: 180px;
- margin: 2ex 0;
- background: #FFFFFF;
- padding-bottom: 120px;
-}
-
-#banner
-{
- text-align: center;
-}
-
-#banner img
-{
- display: block;
- width: 200px;
- margin-left: 10px;
-}
-
-#nav
-{
- margin: 0 1.5ex;
- padding: 0 1.5ex;
-}
-
-#nav h2
-{
- font-size: 0.9em;
- border-bottom: 1px dotted #AAAAAA;
- color: #444444;
- margin: 2ex 0 1ex;
- text-transform: uppercase;
- font-weight: normal;
-}
-
-#nav ul
-{
- list-style: url(../images/arrow.png);
- margin-left: 15px;
-}
-
-#nav li
-{
- margin: 0.25ex 1ex;
- color: #777777;
- font-size: 0.9em;
-}
-
-#nav a
-{
- font-weight: bold;
- margin-right: 0.75ex;
- color: #729FCF;
- font-size: 1.1em;
- display: block;
-}
-
-.home #nav-home a, .get #nav-get a, .join #nav-join a, .help #nav-help a
-{
- color: #777777;
- text-decoration: none;
-}
-
-#content
-{
- display: inline-block;
- margin-left: 230px;
- padding: 1.5ex 2ex 140px 0;
- margin-bottom: 3ex;
- color: #666666;
- background: #FFFFFF;
-}
-
-#content p, #content ul, #content ol
-{
- font-size: 1em;
-}
-
-/* Blame IE6 and the guillotine bug for this one */
-#content { display: block; }
-
-#content h2
-{
- font-size: 1.6em;
- padding-top: .75em;
-}
-
-#content h3
-{
- color: #337acc;
- margin: 1.5ex 0 0.5ex;
- font-size: 1.4em;
-}
-
-#content h3.header {
- display: block;
-}
-
-#content table
-{
- border-collapse: collapse;
-}
-
-#content table th
-{
- background: #DDDDDD;
-}
-
-#content table th, #content table td
-{
- border: 1px solid #000000;
- padding: 0.6ex;
-}
-
-#content p
-{
- margin: 1ex 5ex 1ex 0;
-}
-
-#content ul, #content ol {
- margin: 0 3ex 2ex;
-}
-
-#content ul
-{
- list-style: square;
-}
-
-#content img
-{
- margin: 2ex 0;
-}
-
-
-
-#content .download
-{
- list-style-image: url(../images/arrow.png);
- list-style-position: outside;
- margin: 1.6ex 3ex;
-}
-
-#content .download li
-{
- color: #777777;
- font-size: 1.1em;
- background: #EFEFEF;
- padding: 1ex;
- border-bottom: 1px dotted #AAAAAA;
-}
-
-#content .download a
-{
- font-weight: bold;
-}
-
-#content .roles
-{
- margin: 0!important;
- list-style: none!important;
- display: inline;
-}
-
-#content .roles li
-{
- float: left;
- width: 130px;
- margin-bottom: 0.5ex;
-}
-
-#content .roles a
-{
- line-height: 1;
- height: 150px;
- text-align: center;
- display: block;
- text-decoration: none;
-}
-
-#content .roles a:hover
-{
- background-color: #EFEFEF;
-}
-
-#footer
-{
- position: relative;
- font: normal 0.8em/1.5 sans-serif;
- text-align: center;
- background: white url(../images/line-bottom.png) 0 0 repeat-x;
- margin: -140px 0 0;
- border-bottom: 10px solid #337ACC;
- height: 130px;
- color: #4D4D4D;
-}
-
-#bottom
-{
- margin-left: 18px;
- padding-right: 18px;
-}
-
-#footer ul
-{
- list-style: none;
- margin-bottom: .8em;
-}
-
-#footer li
-{
- display: inline;
- border-left: 1px solid #AAAAAA;
- padding: 0 0.75ex;
- margin-left: -1px;
-}
-
-#footer li.first
-{
- border-left: none;
-}
-
-#footer a
-{
- color: #3771C8;
-}
-
-#footer a:hover
-{
- color: #112233;
-}
-
-#footer p
-{
- margin: 0 1.5ex;
-}
-
-#footer .copy
-{
-}
-
-#footer .rh-logo {
- padding-top: 3ex;
- margin-bottom: 1em;
-}
-s
-#screenshot-banner {
- text-align: left;
-}
-
-input, textarea, select
-{
- border: #AAA 1px solid;
-}
-
-input, textarea, option
-{
- padding: 2px 5px 2px 5px;
-}
-
-#content #sponsors
-{
- list-style: none;
- margin: 0;
- padding: 0;
- overflow: auto;
-}
-
-#content #sponsors li
-{
- text-align: center;
- float: left;
- height: 100px;
- margin: 0 1ex;
-}
-
-#content #sponsors li img
-{
- display: block;
-}
-
-#content #primary-sponsors
-{
- margin: 0 0 4ex;
- padding: 0;
- overflow: auto;
- border-bottom: 1px #AAAAAA solid;
-}
-
-#content #primary-sponsors .psponsor
-{
- text-align: center;
- margin: 0 1ex;
-}
-
-#content #primary-sponsors .psponsor td
-{
- border-style: none;
-}
-
-#content #primary-sponsors .psponsor img
-{
- display: block;
- float: left;
- margin-right: 6ex;
-}
-
-#content #primary-sponsors .psponsor p
-{
- display: block;
- text-align: left;
-}
-
-#content ul.downloadbox
-{
- overflow: hidden;
- list-style: none;
- margin: 0 2ex;
- padding: 6ex 2ex 4ex 180px;
- border-radius: 1.5ex;
- -webkit-border-radius: 1.5ex;
- -moz-border-radius: 1.5ex;
- background: #C0DBDD 30px 50% no-repeat;
- font-size: 0.9em;
- display: inline-block;
-}
-
-#content ul.installdvd { background-image: url(../images/installdvd.png); }
-#content ul.gnomelive { background-image: url(../images/gnomelive.png); }
-#content ul.kdelive { background-image: url(../images/kdelive.png); }
-
-.downloadbox li
-{
- width: 32ex;
- float: left;
- padding: 0 0 2ex;
-}
-
-.downloadbox li li
-{
- width: auto;
- float: none;
- padding: 0;
-}
-
-
-#content pre.command
-{
- font-size: 1.3em;
- border: solid #DDDDDD;
- border-width: 1px 1px 1px 5px;
- background: #F2F5F6;
- padding: 0.5ex 2ex;
-}
-
-#content p.mini-note
-{
- padding: 1ex 2ex;
- border: 1px solid #DDDDDD;
- background: #FFABAB;
- font-size: .8em;
- color: #4c4c4c;
- -webkit-border-radius: 1.5ex;
- -moz-border-radius: 1.5ex;
- width: 80%
-}
-
-#content p.note
-{
- padding: 1ex 2ex;
- border: 1px solid #DDDDDD;
- background: #F2F5F6;
-}
-
-#content p.warning
-{
- padding: 1ex 2ex;
- border: 1px solid #DDDDDD;
- background: #FF6666;
- color: #000000;
-}
-
-#content p.warning a
-{
- color: #00154F;
-}
-#content ul#resources
-{
- list-style: none;
-}
-
-#content ul#resources a
-{
- padding-left: 20px;
- background: url(../images/arrow.png) 0 50% no-repeat;
-}
-
-#content ul#resources li
-{
- padding-left: 70px;
- margin-bottom: 3.5ex;
-}
-
-ul#resources li.resource-docs
-{
- background: url(../images/icon-docs.png) 0 50% no-repeat;
-}
-
-ul#resources li.resource-communicate
-{
- background: url(../images/icon-communicate.png) 0 50% no-repeat;
-}
-
-ul#resources li.resource-download
-{
- background: url(../images/icon-download.png) 0 50% no-repeat;
-}
-
-#content img.smirk
-{
- margin: 0 1ex 0 0;
-}
-
-#content p.joinlink
-{
- border: 1px solid #DDDDDD;
- background: #EEEEEE;
- padding: 1ex 5ex 1ex 5ex;
- text-align: center;
- font-size: 120%;
- font-weight: bold;
-}
-
-#content #subprojects
-{
- margin-right: 5ex;
- padding-top: 1ex;
-}
-
-#content a:link
-{
- color: #337ACC;
- text-decoration: none;
-}
-
-#content a:visited
-{
- color: #729FCF;
- text-decoration: none;
-}
-
-/* get-fedora styles */
-
-.download-block .button:hover {
- background-color: #9ECFFF !important;
-}
-
-.download-block {
- margin-right: 23em;
-}
-
-.download-sidebar {
- float: right;
- width: 21em;
- margin-left: 2em;
-}
-
-.download-sidebar a {
- font-size: 1.5em;
- display: block;
- padding: 1em;
- margin: 1em 0;
-}
-
-#other {
- margin-top: 2ex;
- background-color: #F0F0F0!important;
-}
-
-.download-block h3 {
- margin-top: 0!important;
-}
-
-
-.download-block em.byline {
- display: block;
- color: #999999;
- font-size: 0.8em;
-}
-
-.download-block em.download-tagline {
- color: #999999;
- font-size: 0.8em;
-}
-
-.download-block .button {
- border-radius: 5px;
- -webkit-border-radius: 5px;
- -moz-border-radius: 5px;
- color: #FFFFFF!important;
- text-decoration: none;
- font-weight: bold;
- line-height: 3;
- margin: 0;
- padding: 0.4em 0.5em;
- border: 1px solid #446382;
- background-color: #5F94C9 !important;
- margin-top: 0.9em;
-}
-
-.big-button {
- border-radius: 5px;
- -webkit-border-radius: 5px;
- -moz-border-radius: 5px;
- color: #FFFFFF!important;
- text-decoration: none;
- font-weight: bold;
- line-height: 3;
- margin: 0;
- padding: 0.4em 0.5em;
- background-color: #337acc !important;
- margin-top: 0.9em;
- font-size: 1.7em;
-}
-
-.big-button:hover {
- background-color: #295596 !important;
-}
-
-
-.fine-print {
- font-size: .9em !important;
- color: #999999;
- font-style: italic;
-}
-
-.resources-pane {
- float: right;
- background-color: #E5E5E5;
- padding: 0.5em 1em;
- border-radius: 5px;
- -webkit-border-radius: 5px;
- -moz-border-radius: 5px;
- margin-left: 1em;
-}
-
-.download-block p img {
- float: left;
- margin-right: 1.4em !important;
-}
-
-/** sidebar boxes for get-fedora **/
-
-.download-sidebar a {
- display: block;
- background: #337ACC url(../images/gradient_bg.png) repeat-x;
- border-radius: 8px;
- -webkit-border-radius: 8px;
- -moz-border-radius: 8px;
-}
-
-.download-sidebar a.even {
- background-color: #1855A2;
-}
-
-.download-sidebar a:hover {
- background-color: #1158AA;
-}
-
-.download-sidebar a.even:hover {
- background-color: #003380;
-}
-
-.download-sidebar a {
- color: #FFFFFF!important;
- font-size: medium;
- text-decoration: none;
- font-weight: bold;
- line-height: 1.5;
-}
-
-.download-sidebar span#thereismorefedora a {
- display: block;
- background: none;
- margin: 0px;
- padding: 0px;
-}
-
-.download-sidebar span#thereismorefedora img {
- margin: 0px;
- padding: 0px;
-}
-
-#breadcrumbs {
- border-bottom: 1px solid #DDDDDD;
- margin-bottom: 1ex;
- margin-right: 24%;
-}
-
-#breadcrumbs a {
- text-decoration: none;
- font-size: 1em;
- color: #7788DD;
-}
-
-/** Web App Styles **/
-/* Links to other web apps */
-a.bodhi {
- padding-left: 20px;
- background: url(../images/icons/fedora-infra-icon_bodhi.png) 0 50% no-repeat;
-}
-
-a.koji {
- padding-left: 20px;
- background: url(../images/icons/fedora-infra-icon_koji.png) 0 50% no-repeat;
-}
-
-a.bugz {
- padding-left: 20px;
- background: url(../images/icons/fedora-infra-icon_bugzilla.png) 0 50% no-repeat;
-}
-
-a.pkgdb {
- padding-left: 20px;
- background: url(../images/icons/fedora-infra-icon_pkgdb.png) 0 50% no-repeat;
-}
-
-a.cvs {
- padding-left: 20px;
- background: url(../images/icons/fedora-infra-icon_source-control.png) 0 50% no-repeat;
-}
-
-/* tooltip */
-.tooltip {
- display: block;
- position: absolute;
- border: 1px solid black;
- background-color: #ffffdd;
- width: 200px;
-}
-
-/* style to hide an element */
-.invisible {
- display: none;
-}
-
-/* styles for tg flash messages */
-.flash {
- background: #DEE681;
- border: 1px solid #CCBBAA;
- padding: 1.5ex 15px 1.5ex 43px;
- margin: 1ex 0;
-}
-
-.flash .success {
- background: #DEE681 url(../images/success.png) 10px 50% no-repeat;
-}
-
-.flash .warning {
- background: #DEE681 url(../images/important.png) 10px 50% no-repeat;
-}
-
-.flash .info {
- background: #DEE681 url(../images/info.png) 10px 50% no-repeat;
-}
-
-/* toolbar styles */
-.toolbar {
- list-style: none;
- list-style-image: none;
- list-style-position: outside;
- list-style-type: none;
- text-align: right;
- color: #888888;
- font-size: 0.9em;
- text-transform: uppercase;
- vertical-align: middle;
-}
-
-.toolbar * {
- display: inline;
-}
-
-#head #main-toolbar {
- float: right;
- margin-top: 9px;
- width: 50%;
-}
-
-.toolbar input.button {
- background-color: #888888;
- color: white;
- font-size: 0.9em;
- margin-left: 5px;
- padding: 2px;
- text-transform: uppercase;
- vertical-align: middle;
-}
-
-/* panel styles */
-#content .panel {
- border: medium none;
- margin-top: 12px;
-}
-
-#content .panel h3 {
- -moz-border-radius-bottomleft: 0;
- -moz-border-radius-bottomright: 0;
- -moz-border-radius-topleft: 12px;
- -moz-border-radius-topright: 12px;
- background-color: #D2D2D2;
- color: white;
- font-size: 1em;
- padding: 8px 6px 6px;
- text-transform: uppercase;
- margin: 0;
-}
-
-/* Login panel */
-#login-box form .field {
- margin: 0 0 1ex;
- overflow: hidden;
- text-align: left;
-}
-
-#login-box .field label {
- clear: left;
- float:left;
- padding: 0 2ex 0 0;
- text-align: left;
- width: 32ex;
-}
-
-#content .login {
- -moz-border-radius-bottomleft: 10px;
- -moz-border-radius-bottomright: 10px;
- -moz-border-radius-topleft: 10px;
- -moz-border-radius-topright: 10px;
- background-color: #5F94C9;
- color: white;
- padding: 8px 12px;
-}
-
-#content div.login * {
- color: white;
- padding: 0;
- margin: 0;
-}
-
-#content .login h3 {
- background-color: #5F94C9;
-}
-
-#content .login input {
- padding: 2px 5px;
- margin-bottom: 5px;
- color: black;
-}
-
-#content .login label {
- color: white;
- display: block;
- float: left;
- font-size: 0.8em;
- margin-right: 10px;
- width: 60px;
- text-align: left;
-}
-
-#content .login .button {
- background-color: #777777;
- border: 1px solid #555555;
- color: white;
-}
-
-#content .login ul {
- list-style-position: inside;
-}
-
-#export-regulations {
- padding-top: 1em;
- margin-left: 2em;
-}
-
-#export-regulations .read-more img, #export-regulations .details img {
- margin: 0;
-}
-
-#export-regulations .details {
- display: inline;
-}
-
-#export-regulations a {
- text-decoration: none;
-}
-
-.read-more, .re-collapse {
- float:right;
-}
-
-.read-more img {
- float: none;
-}
-
-/* applies to get-* pages */
-
-#download-resources {
- float:right;
- margin-left:.5em;
- width:21em; /* tofix */
-}
-
-#download-resources h4 {
- text-transform: uppercase;
- margin-bottom: .5em;
-}
-
-#download-resources li {
- padding: 3px 0;
-}
-
-#download-resources ul.checkmark-list {
- list-style: disc url("/static/images/icon-checkmark.png") outside;
-}
-
-#download-resources li.guide-list {
- list-style: disc url("/static/images/icon-guide.png") outside;
-}
-
-#download-resources li.webpage-list {
- list-style: disc url("/static/images/icon-webpage.png") outside;
-}
-
-#download-resources li.support-list {
- list-style: disc url("/static/images/icon-support.png") outside;
-}
-
-#download-resources li.verify-list {
- list-style: disc url("/static/images/icon-signed-verified.png") outside;
-}
-
-#download-resources div, #download-resources div h4 {
- padding: 5px 0 5px 0;
-}
-
-#download-resources div p {
- padding-left: 5px;
-}
-
-#download-resources p,
-#download-resources ul {
- margin-bottom: 2em;
-}
-
-#download-resources ul {
- margin-left: 2em;
-}
-
-#download-options-area {
- background-color: #f2f2f2;
- padding: 15px;
-}
-
-#download-options-area h4 {
- color: #337ACC;
-}
-
-.other-options {
- float: right;
- border-radius: 5px;
- -webkit-border-radius: 5px;
- -moz-border-radius: 5px;
- margin-left: 1em;
- width: 45%;
-}
-
-.upgrade-section {
- width: 50%;
-}
-
-/* get-* format tab */
-.format-arch {
- border-bottom: 1px #E5E5E5 solid;
- float:left;
- width: 90%;
- padding: 5px;
-}
-
-.support-icon {
- border: 0px;
- padding: 3px;
-}
-
-.vendor-header {
- clear: left;
- font-size: 1.3em;
- padding-top: 15px;
- padding-bottom: 15px;
-}
-
-.freemedia-header {
- clear: left;
- font-size: 1.3em;
- padding-top: 15px;
-}
-
-
-.vendor1, .vendor2 {
- width: 50%;
-}
-
-/* end: applies to get-* pages */
-
-/* tabs */
-/* root element for tabs */
-ul.tabs {
- margin: 0 !important;
- padding: 0;
- height: 30px;
- border-bottom: 1px solid #dcdcdc;
- width: 70%;
-}
-
-/* single tab */
-ul.tabs li {
- float: left;
- padding: 0;
- margin: 0;
- list-style-type: none;
-}
-
-/* link inside the tab. no background image */
-ul.tabs a {
- float: left;
- font-size: 13px;
- display: block;
- padding: 5px 10px;
- text-decoration: none;
- border: 1px solid #dcdcdc;
- border-bottom: 0px;
- height: 18px;
- background-color: #dcdcdc;
- color: #3771c8;
- margin-right: 4px;
- -moz-border-radius-topleft: 4px;
- -moz-border-radius-topright: 4px;
- position: relative;
- top: 1px;
- text-transform: uppercase;
- font-weight: bold;
-}
-
-ul.tabs a:hover {
- background-color: #fff;
- color: #818181;
- text-transform: uppercase;
- font-weight: bold;
-}
-
-/* selected tab */
-ul.tabs a.current {
- background-color: #fff;
- border-bottom: 2px solid #fff;
- color: #808080;
- cursor: default;
- text-transform: uppercase;
- font-weight: bold;
-}
-
-/* tab pane */
-div.panes div {
- display: none;
- border: 0px;
- border-left: 1px solid #dcdcdc;
- min-height: 150px;
- padding: 15px 10px;
- /* tofix width: 70%; */
- float: left;
-}
-
-#content a.current:link {
- color: #828282;
-}
-/* end: tabs */
-
-/* --- */
-#content span.download-opt1
-{
- float: left;
- width: 45%;
-}
-
-div.panes span.download-option {
- display: block;
- /* tofix: width: 300px !important; */
- width: 50%;
-}
-
-div.panes span.opt1 { float: left; }
-div.panes span.opt2 {
- float: right;
- width: 50%;
-}
-
-#export-regulations {
- clear: both;
- margin-bottom: 2em;
-}
-
-/* Hack to make it simple to get rid of borders in a table (ianweller) */
-#content table.nobordersplz th, #content table.nobordersplz td { border-width: 0px; }
-
-/* get-option common */
-/* keeps common stuff in get pages */
-.star-fedora-default, .star-most-compatible {
- background-color: #F7FF9D;
- border: 1px solid #A19800;
- padding: 0.2em;
- font-weight: bold;
- color: #A19800;
- font-size: 0.8em;
- margin: 7px;
- text-transform: uppercase;
- white-space: nowrap;
-}
-
-.download-button-mini {
- border-radius: 5px;
- -webkit-border-radius: 5px;
- -moz-border-radius: 5px;
- color: #FFFFFF!important;
- text-decoration: none;
- font-weight: bold;
- line-height: 2;
- font-size:0.85em;
- padding: 5px;
- background-color: #337ACC !important;
- white-space: nowrap;
-}
-
-.download-button-mini:hover {
- background-color: #295596 !important;
-}
-
-.opt1 { float: left; }
-.opt2 { float: right; }
-
-#content .format-arch .opt1 p, #content .format-arch .opt1 h3, #content .format-arch .opt2 p, #content .format-arch .opt2 h3 {
- padding: 0px;
- margin: 0px
-}
-
-.panes {
- width: 68%
-}
-
-.shownojs { display: none; }
-
-.area-callout
-{
- display: inline-block; /* to put icons next to header */
-}
-
-/* end: get-option common */
-
-/* get-option - desktop pane */
-#content .desktop-section table, #content .desktop-section table td {
- border: 0px;
-}
-
-.desktop-section ul {
- float: left;
-}
-
-.desktop-section img.desktop-screenshot {
- float: right;
-}
-
-.desktop-section table tbody tr {
- background-color: #fff;
-}
-
-.desktop-section td.description {
- max-width: 230px;
-}
-
-.desktop-section td {
- vertical-align: top;
-}
-
-img.desktop-screenshot:hover {
- cursor: pointer;
- cursor: hand;
-}
-
-/* end: get-option - desktop pane */
-
-
-/* get-option - architecture pane */
-.arch-section, .desktop-section {
- clear:left;
- display:block;
- margin-bottom:1em;
- padding:0.5em 1em;
-}
-
-img.download-media
-{
- float: left;
- padding-right: 7px;
-}
-
-.arch-sparc-area {
- background-color: #f5f5f5;
- padding: 1em;
- clear: both;
- display: block;
-}
-
-.arch-section h3 span {
- color: black;
-}
-
-/* end: get-option - architecture pane */
-
-.panes table tr {
- background-color:#F2F2F2;
- border:0 none;
-}
-
-#get-acitivites td.rank {
- background-color:white;
- font-weight:bold;
- padding-left:0;
- padding-right:7px;
- text-align:left;
-}
-
-#get-acitivites td.name {
- font-size:xx-small;
- font-style:italic;
-}
-
-#get-acitivites table td {
- border-color: white !important;
- border-style: solid !important;
- border-width: 0 0 18px !important;
- padding: 0.8em;
- vertical-align: top;
-}
-
-.learn-more {
- display: none;
-}
-
-/*
- * note: probably not needed
-#content table th, #content table td {
- border:1px solid #000000;
- padding:0.6ex;
-}
-*/
-
-/* end: get-option - activites pane */
-
-#content img {
- margin:0;
-}
-
-
-
-/* make it work in 1024x786 */
-
-#desktop-versions {
- width: 100%;
-}
-
-.panes {
- min-width: 50%;
-}
-
-.panes table {
- width: 80%;
-}
-
-#download-resources {
- min-width: 200px;
- width: 20%;
-}
-
-
-#content .panes {
- min-width: 400px;
-}
-
-#nav {
- margin: 0 1em;
- padding:0 0 0 1.5em;
-}
-
-tr.dl-buttons td {
- min-width: 104px !important;
-}
-
-div.panes div {
- background-color: transparent !important;
- padding-right: 15 5 15 10;
-}
-
-.arch-section,
-.desktop-section {
- padding-right: 0.5em 0 0.5em 1em;
-}
-
-/* the overlayed element */
-.simple_overlay {
-
- /* must be initially hidden */
- display:none;
-
- /* place overlay on top of other elements */
- z-index:10000;
-
- /* styling */
- background-color:#333;
-
- /* disabled for now - so it fits around images
- * width:675px;
- */
- min-height:200px;
- border:1px solid #666;
-
- /* CSS3 styling for latest browsers */
- -moz-box-shadow:0 0 90px 5px #000;
- -webkit-box-shadow: 0 0 90px #000;
-}
-
-/* close button positioned on upper right corner */
-.simple_overlay .close {
- background-image:url(../images/overlay-close.png);
- position:absolute;
- right:-15px;
- top:-15px;
- cursor:pointer;
- height:35px;
- width:35px;
-}
-
-/* styling for elements inside overlay */
-.overlay_details {
- position:absolute;
- top:15px;
- right:15px;
- font-size:11px;
- color:#fff;
- width:150px;
-}
-
-.overlay_details h3 {
- color:#aba;
- font-size:15px;
- margin:0 0 -10px 0;
-}
-
-/* end: the overlayed element */
-
-.global-notice {
- background-color: #f6f6f6;
- border: 1px solid #ddd;
- padding: 0.5em 1em;
-}
diff --git a/httpd-fedora-elections.conf.in b/httpd-fedora-elections.conf.in
index f681506..c974b88 100644
--- a/httpd-fedora-elections.conf.in
+++ b/httpd-fedora-elections.conf.in
@@ -1,4 +1,5 @@
-Alias /fedora-elections/static @DATADIR@/fedora_elections/static
+Alias /fedora-elections/images @DATADIR@/fedora-elections/images
+Alias /fedora-elections/static @DATADIR@/fedora-elections/static
WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
WSGISocketPrefix run/wsgi
@@ -6,7 +7,7 @@ WSGIRestrictStdout On
WSGIRestrictSignal Off
WSGIPythonOptimize 1
-WSGIScriptAlias /fedora-elections @SBINDIR(a)/fedora-elections.wsgi
+WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora-elections/fedora-elections.wsgi
<Location /fedora-elections>
WSGIProcessGroup fedora-elections
commit b4fd5269695344e0c634b78a16df2dc93be8f090
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Thu May 9 03:52:52 2013 +0000
Change for RPM
diff --git a/fedora-elections.cfg.sample b/fedora-elections.cfg.sample
new file mode 100644
index 0000000..1dff33a
--- /dev/null
+++ b/fedora-elections.cfg.sample
@@ -0,0 +1,18 @@
+import os, os.path
+
+DEBUG = True
+
+
+# You will want to change this for your install
+SECRET_KEY = 'change me'
+SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.getcwd(), 'database.sqlite')
+
+FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
+FAS_USERNAME = 'elections'
+FAS_PASSWORD ='fake_fas_password'
+FAS_CHECK_CERT = False
+FAS_HTTPS_REQUIRED = False
+
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+# List of URLs for additional site-wide CSS
+EXTRA_CSS = []
commit 07d831bc8b7c5bae9e56d6b1e2950b99b90df473
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Thu May 9 03:52:20 2013 +0000
Change for RPM
diff --git a/fedora-elections.spec b/fedora-elections.spec
index c24cb39..7eff44d 100644
--- a/fedora-elections.spec
+++ b/fedora-elections.spec
@@ -6,35 +6,58 @@ Summary: Fedora elections application
Group: Development/Languages
License: GPLv2+
URL: https://github.com/fedora-infra/elections
-Source0: https://fedorahosted.org/releases/f/a/%{name}/%{name}-%{version}.tar.gz
-
+Source0: %{name}/%{name}-%{version}.tar.gz
+# Source0: https://fedorahosted.org/releases/f/a/%{name}/%{name}-%{version}.tar.gz
BuildArch: noarch
+BuildRequires: libxslt
BuildRequires: python
-BuildRequires: python-devel
-BuildRequires: python-beaker
+BuildRequires: python-babel
+BuildRequires: python-backports-ssl_match_hostname
+BuildRequires: python-bunch
+BuildRequires: python-chardet
BuildRequires: python-fedora
BuildRequires: python-fedora-flask
BuildRequires: python-flask
-BuildRequires: python-flask-babel
BuildRequires: python-flask-sqlalchemy
-BuildRequires: python-flask-wtforms
+BuildRequires: python-flask-wtf
+BuildRequires: python-jinja2
BuildRequires: python-kitchen
+BuildRequires: python-lxml
+BuildRequires: python-openid
+BuildRequires: python-ordereddict
+BuildRequires: python-requests
BuildRequires: python-setuptools
-BuildRequires: python-setuptools-devel
+BuildRequires: python-simplejson
+BuildRequires: python-six
BuildRequires: python-sqlalchemy0.7
-
-Requires: python-beaker
+BuildRequires: python-urllib3
+BuildRequires: python-wtforms
+
+Requires: libxslt
+Requires: python
+Requires: python-babel
+Requires: python-backports-ssl_match_hostname
+Requires: python-bunch
+Requires: python-chardet
Requires: python-fedora
Requires: python-fedora-flask
Requires: python-flask
-Requires: python-flask-babel
Requires: python-flask-sqlalchemy
-Requires: python-flask-wtforms
+Requires: python-flask-wtf
Requires: python-jinja2
Requires: python-kitchen
+Requires: python-lxml
+Requires: python-openid
+Requires: python-ordereddict
+Requires: python-requests
+Requires: python-setuptools
+Requires: python-simplejson
+Requires: python-six
Requires: python-sqlalchemy0.7
+Requires: python-urllib3
+Requires: python-wtforms
%description
@@ -42,6 +65,7 @@ fedora-elections is the Fedora Elections application.
%prep
%setup -q
+./configure.py
%build
@@ -58,7 +82,8 @@ rm -rf $RPM_BUILD_ROOT
%{__mkdir_p} %{buildroot}%{_datadir}/%{name}
%{__mkdir_p} %{buildroot}%{_datadir}/%{name}/static
-%{__install} -m 644 fedora_elections.conf %{buildroot}%{_sysconfdir}/httpd/conf.d/fedora_elections.conf
+%{__install} -m 644 httpd-fedora-elections.conf \
+ %{buildroot}%{_sysconfdir}/httpd/conf.d/httpd-fedora-elections.conf
%{__install} -m 644 fedora_elections/static/* %{buildroot}%{_datadir}/%{name}/static
%{__install} -m 644 %{name}.cfg.sample %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
%{__install} -m 644 createdb.py %{buildroot}%{_datadir}/%{name}/createdb.py
@@ -75,11 +100,11 @@ rm -rf $RPM_BUILD_ROOT
%doc
%dir %{_sysconfdir}/%{name}
%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
-%config(noreplace) %{_sysconfdir}/httpd/conf.d/%{name}.conf
+%config(noreplace) %{_sysconfdir}/httpd/conf.d/httpd-%{name}.conf
%{_datadir}/%{name}
%{python_sitelib}/*
%changelog
-* Wed Mar 06 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1
+* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1
- Creation
diff --git a/setup.py b/setup.py
index e5ffd25..83a6354 100644
--- a/setup.py
+++ b/setup.py
@@ -12,5 +12,5 @@ setup( name = 'fedora-elections'
, packages = find_packages()
, zip_safe = False
, include_package_data = True
- , install_requires = ['Flask', 'SQLAlchemy>=0.7', 'flask-sqlalchemy', 'beaker']
+ , install_requires = ['Flask', 'SQLAlchemy>=0.7', 'flask-sqlalchemy']
)
commit 3cb86d139e47811f05d0a5a450afebde7d0631c4
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Tue Apr 30 04:02:50 2013 +0000
httpd/wsgi now working
diff --git a/.gitignore b/.gitignore
index bb585f9..7ecf5f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,7 @@ database.sqlite
*.pyc
*.pyo
*.swp
+development.cfg
+fedora-elections.cfg
+fedora-elections.wsgi
+httpd-fedora-elections.conf
diff --git a/README b/README
new file mode 100644
index 0000000..69d11ea
--- /dev/null
+++ b/README
@@ -0,0 +1,95 @@
+================
+Fedora Elections
+===============
+
+* What is fedora-election?
+fedora-elections is a web application written in python and based on flask.
+It implements the Range Voting system (http://rangevoting.org).
+
+This project was developed using fedoraproject requests, but, can be easily
+adapted to other projects. Fedora-elections is integrated with the Fedora
+Account System (FAS).
+
+
+Running from a checkout:
+=======================
+
+Install Prerequisites
+---------------------
+Before launching fedora-elections, the following packages should be installed:
+ - httpd
+ - libxslt
+ - python
+ - python-backports-ssl_match_hostname
+ - python-bunch
+ - python-chardet
+ - python-fedora
+ - python-fedora-flask
+ - python-flask
+ - python-flask-sqlalchemy
+ - python-flask-wtf
+ - python-jinja2
+ - python-kitchen
+ - python-lxml
+ - python-openid
+ - python-ordereddict
+ - python-ordereddict
+ - python-setuptools
+ - python-simplejson
+ - python-six
+ - python-sqlalchemy0.7
+ - python-urllib3
+ - python-wtforms
+
+
+Get the source code
+-------------------
+You can obtain the code from github:
+ https://github.com/fedora-infra/elections.git
+
+
+Configure the application
+-------------------------
+./configure.py --install-conf=`pwd` --install-data=`pwd` --install-sbin=`pwd`
+
+This will create fedora-elections.wsgi and http-fedora-elections.conf.
+
+
+Create a database
+-----------------
+Copy development.cfg.sample to development.cfg.
+Review the file and make changes as appropriate.
+Run:
+ ./createdb
+
+
+Starting the Application
+------------------------
+
+There are 2 ways to start the application:
+ o without http
+ o with http
+
+
+* How to start without http on localhost:5000:
+ cd elections
+ ./runserver
+
+
+* How to start with http
+ Next copy the httpd-pkgdb.conf file to your apache conf.d directory:
+
+ sudo cp httpd-fedora-elections.conf /etc/httpd/conf.d/.
+
+ Restart apache:
+
+ sudo /etc/init.d/httpd restart
+
+* How to contribute
+If you find bug or want to propose ideas or stuff to be implemented or if
+you are interested to became a developer for this project just
+ask on #fedora-admin irc channel on irc.freenode.net or use our
+web site https://fedorahosted.org/elections.
+
+* Licence
+fedora-elections is licenced under GPL v2.
diff --git a/configure.py b/configure.py
new file mode 100755
index 0000000..9ea3f51
--- /dev/null
+++ b/configure.py
@@ -0,0 +1,77 @@
+#!/usr/bin/python -tt
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2013 Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+#
+# Author(s): Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+
+import glob
+import os
+import re
+
+from optparse import OptionParser
+
+#
+# Process any command line options/switches.
+#
+parser = OptionParser()
+parser.add_option('--install-conf', dest='confdir', action='store',
+ default='/etc',
+ help='Where to install configuration files.')
+parser.add_option('--install-data', dest='datadir', action='store',
+ default='/usr/share',
+ help='Where to install data files.')
+parser.add_option('--install-sbindir', dest='sbindir', action='store',
+ default='/usr/sbin',
+ help='Where to install executeable files.')
+
+(options, args) = parser.parse_args()
+
+confdir = options.confdir
+datadir = options.datadir
+sbindir = options.sbindir
+
+print " confdir = %s" % confdir
+print " datadir = %s" % datadir
+print " sbindir = %s" % sbindir
+
+substs = {'@CONFDIR@':confdir,
+ '@DATADIR@':datadir,
+ '@SBINDIR@':sbindir
+ }
+
+pattern = re.compile("\.in$")
+
+files = glob.glob('./*.in')
+
+#
+# Process all *.in files in the current directory.
+#
+if (files is not []):
+ for orig_fn in files:
+ if os.path.isfile(orig_fn):
+ new_fn = re.sub(pattern, '', orig_fn)
+ print " Creating %s from %s" % (new_fn, orig_fn)
+ fp1 = open(orig_fn, "r")
+ file_contents = fp1.read()
+ fp1.close()
+
+ fp2 = open(new_fn, "w")
+ for key, value in substs.iteritems():
+ file_contents = file_contents.replace(key, value)
+ fp2.write(file_contents)
+ fp2.close()
diff --git a/development.cfg.sample b/development.cfg.sample
new file mode 100644
index 0000000..1dff33a
--- /dev/null
+++ b/development.cfg.sample
@@ -0,0 +1,18 @@
+import os, os.path
+
+DEBUG = True
+
+
+# You will want to change this for your install
+SECRET_KEY = 'change me'
+SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.getcwd(), 'database.sqlite')
+
+FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
+FAS_USERNAME = 'elections'
+FAS_PASSWORD ='fake_fas_password'
+FAS_CHECK_CERT = False
+FAS_HTTPS_REQUIRED = False
+
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+# List of URLs for additional site-wide CSS
+EXTRA_CSS = []
diff --git a/fedora-elections.cfg.in b/fedora-elections.cfg.in
new file mode 100644
index 0000000..fc0d50d
--- /dev/null
+++ b/fedora-elections.cfg.in
@@ -0,0 +1,18 @@
+import os, os.path
+
+DEBUG = True
+
+
+# You will want to change this for your install
+SECRET_KEY = 'change me'
+SQLALCHEMY_DATABASE_URI = 'sqlite:///@DATADIR@/database.sqlite'
+
+FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
+FAS_USERNAME = 'elections'
+FAS_PASSWORD ='el3ctMEn0w'
+FAS_CHECK_CERT = False
+FAS_HTTPS_REQUIRED = False
+
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+# List of URLs for additional site-wide CSS
+EXTRA_CSS = []
diff --git a/fedora-elections.spec b/fedora-elections.spec
new file mode 100644
index 0000000..c24cb39
--- /dev/null
+++ b/fedora-elections.spec
@@ -0,0 +1,85 @@
+Name: fedora-elections
+Version: 0.1
+Release: 1%{?dist}
+Summary: Fedora elections application
+
+Group: Development/Languages
+License: GPLv2+
+URL: https://github.com/fedora-infra/elections
+Source0: https://fedorahosted.org/releases/f/a/%{name}/%{name}-%{version}.tar.gz
+
+
+BuildArch: noarch
+
+BuildRequires: python
+BuildRequires: python-devel
+BuildRequires: python-beaker
+BuildRequires: python-fedora
+BuildRequires: python-fedora-flask
+BuildRequires: python-flask
+BuildRequires: python-flask-babel
+BuildRequires: python-flask-sqlalchemy
+BuildRequires: python-flask-wtforms
+BuildRequires: python-kitchen
+BuildRequires: python-setuptools
+BuildRequires: python-setuptools-devel
+BuildRequires: python-sqlalchemy0.7
+
+Requires: python-beaker
+Requires: python-fedora
+Requires: python-fedora-flask
+Requires: python-flask
+Requires: python-flask-babel
+Requires: python-flask-sqlalchemy
+Requires: python-flask-wtforms
+Requires: python-jinja2
+Requires: python-kitchen
+Requires: python-sqlalchemy0.7
+
+
+%description
+fedora-elections is the Fedora Elections application.
+
+%prep
+%setup -q
+
+
+%build
+# Remove CFLAGS=... for noarch packages (unneeded)
+CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
+
+
+%install
+rm -rf $RPM_BUILD_ROOT
+%{__python} setup.py install -O1 --skip-build --install-data=%{_datadir} --root $RPM_BUILD_ROOT
+
+%{__mkdir_p} %{buildroot}%{_sysconfdir}/%{name}
+%{__mkdir_p} %{buildroot}%{_sysconfdir}/httpd/conf.d
+%{__mkdir_p} %{buildroot}%{_datadir}/%{name}
+%{__mkdir_p} %{buildroot}%{_datadir}/%{name}/static
+
+%{__install} -m 644 fedora_elections.conf %{buildroot}%{_sysconfdir}/httpd/conf.d/fedora_elections.conf
+%{__install} -m 644 fedora_elections/static/* %{buildroot}%{_datadir}/%{name}/static
+%{__install} -m 644 %{name}.cfg.sample %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
+%{__install} -m 644 createdb.py %{buildroot}%{_datadir}/%{name}/createdb.py
+%{__install} -m 644 %{name}.wsgi %{buildroot}%{_datadir}/%{name}/%{name}.wsgi
+
+
+
+%clean
+rm -rf $RPM_BUILD_ROOT
+
+
+%files
+%defattr(-,root,root,-)
+%doc
+%dir %{_sysconfdir}/%{name}
+%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
+%config(noreplace) %{_sysconfdir}/httpd/conf.d/%{name}.conf
+%{_datadir}/%{name}
+%{python_sitelib}/*
+
+
+%changelog
+* Wed Mar 06 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1
+- Creation
diff --git a/fedora-elections.wsgi.in b/fedora-elections.wsgi.in
new file mode 100644
index 0000000..c7cfbb6
--- /dev/null
+++ b/fedora-elections.wsgi.in
@@ -0,0 +1,22 @@
+#-*- coding: UTF-8 -*-
+
+# These two lines are needed to run on EL6
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import os
+import sys
+
+sys.stdout = sys.stderr
+
+## Set the environment variable pointing to the configuration file
+os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections.cfg'
+
+## The following is only needed if you did not install
+## as a python module (for example if you run it from a git clone).
+import sys
+# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
+sys.path.insert(0, '@SBINDIR@')
+
+from fedora_elections import app as application
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index e0b4ef5..18cf36f 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -96,7 +96,9 @@ def election_admin_required(f):
@app.route('/')
def index():
elections = models.Election.query.filter_by(frontpage=True).all()
+ num_elections = len(elections)
return flask.render_template('list/index.html', elections=elections,
+ num_elections=num_elections,
title="Elections")
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 2985fcc..290c2cf 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -1,5 +1,6 @@
{% extends "base.html" %}
{% block body %}
+ {% if num_elections > 0 %}
<h2>{{ title }}</h2>
<table>
<tr>
@@ -27,4 +28,5 @@
</tr>
{% endfor %}
</table>
+ {% endif %}
{% endblock %}
diff --git a/httpd-fedora-elections.conf.in b/httpd-fedora-elections.conf.in
new file mode 100644
index 0000000..f681506
--- /dev/null
+++ b/httpd-fedora-elections.conf.in
@@ -0,0 +1,22 @@
+Alias /fedora-elections/static @DATADIR@/fedora_elections/static
+
+WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
+WSGISocketPrefix run/wsgi
+WSGIRestrictStdout On
+WSGIRestrictSignal Off
+WSGIPythonOptimize 1
+
+WSGIScriptAlias /fedora-elections @SBINDIR(a)/fedora-elections.wsgi
+
+<Location /fedora-elections>
+ WSGIProcessGroup fedora-elections
+ <IfModule mod_authz_core.c>
+ # Apache 2.4
+ Require all granted
+ </IfModule>
+ <IfModule !mod_authz_core.c>
+ # Apache 2.2
+ Order deny,allow
+ Allow from all
+ </IfModule>
+</Location>
commit 0316694d783556284b54b6aa70bb2c80cdaa05b4
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Wed Feb 27 23:29:37 2013 -0500
Correct version
diff --git a/setup.py b/setup.py
index 47357d3..e5ffd25 100644
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ import pkg_resources
from setuptools import setup, find_packages
setup( name = 'fedora-elections'
- , version = '@VERSION@'
+ , version = '0.1'
, author = 'Frank Chiulli'
, author_email = 'fchiulli(a)fedoraproject.org'
, packages = find_packages()
commit 41cb17c7ee7ab04e1d3e77cbb79081ba9425efeb
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Wed Feb 27 22:35:59 2013 -0500
Update documentation
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..0f3bd00
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,4 @@
+Ralph Bean <rbean(a)redhat.com>
+Frank Chiulli <fchiulli(a)fedoraproject.org>
+Toshio Kuratomi <toshio(a)fedoraproject.org>
+Ian Weller <ianweller(a)fedoraproject.org>
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..7a12f1b
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,343 @@
+ GNU GENERAL PUBLIC LICENSE
+Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software
+ interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Program does not specify a
+version number of this License, you may choose any version ever
+published by the Free Software Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+END OF TERMS AND CONDITIONS
+
+How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these
+terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..47357d3
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,16 @@
+## These two lines are needed to run on EL6
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+
+from setuptools import setup, find_packages
+
+setup( name = 'fedora-elections'
+ , version = '@VERSION@'
+ , author = 'Frank Chiulli'
+ , author_email = 'fchiulli(a)fedoraproject.org'
+ , packages = find_packages()
+ , zip_safe = False
+ , include_package_data = True
+ , install_requires = ['Flask', 'SQLAlchemy>=0.7', 'flask-sqlalchemy', 'beaker']
+ )
commit f324f1c5ea5482fcf640c35d288593ed5b8a817b
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Wed Feb 27 22:35:32 2013 -0500
Fix AccountSystem call
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index d2ded05..e0b4ef5 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -56,7 +56,8 @@ fas = FAS(app)
fas2_url = app.config['FAS_BASE_URL']
fas2_username = app.config['FAS_USERNAME']
fas2_password = app.config['FAS_PASSWORD']
-fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password)
+fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password,
+ insecure=True)
# modular imports
commit e65990eaa653054e4be9848c12aad5dbb593ad23
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Sun Feb 17 23:01:56 2013 -0500
Test again
commit 1b8e2ecf739de797401e9539cb94c02855790e55
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Sun Feb 17 22:49:05 2013 -0500
Test git push
commit c961c2cde24f34c8d0c930ab696ca1559a3e1823
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Sun Feb 17 19:05:01 2013 -0500
Lots of updates/changes
diff --git a/TODO b/TODO
index 9858c8f..ebace48 100644
--- a/TODO
+++ b/TODO
@@ -2,8 +2,6 @@ Things that need doing to get this deployed
===========================================
* Add legalvoters to an election
-* Add page to vote
-* Add page that summarizes the vote results
* Write a conversion script for the old db model to the new one
* rpmify
* rpmify flask-fas (or get flask-fas into python-fedora)
@@ -19,7 +17,6 @@ Future enhancements (anything not necessary for deploying)
==========================================================
* Make voting methods modular
-* Give this a Fedora theme
* Break the main program logic out into library modules
** Controllers should be as lightweight as possible
*** validation of input
diff --git a/development.cfg b/development.cfg
index 6e045f2..e4b83bb 100644
--- a/development.cfg
+++ b/development.cfg
@@ -1,11 +1,15 @@
import os, os.path
DEBUG = True
+
+
# You will want to change this for your install
SECRET_KEY = 'change me'
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.getcwd(), 'database.sqlite')
FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
+FAS_USERNAME = 'elections'
+FAS_PASSWORD ='el3ctMEn0w'
FAS_CHECK_CERT = False
FAS_HTTPS_REQUIRED = False
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 78d2a16..d2ded05 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -1,7 +1,37 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2012 Red Hat, Inc.
+# Copyright © 2012 Ian Weller <ianweller(a)fedoraproject.org>
+# Copyright © 2012 Toshio Kuratomi <tkuratom(a)redhat.com>
+# Copyright © 2012 Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+#
+# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
+# Toshio Kuratomi <tkuratom(a)redhat.com>
+# Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+
import flask
from flask.ext.fas import FAS
from flask.ext.sqlalchemy import SQLAlchemy
+from fedora.client import AuthError, AppError
+from fedora.client.fas2 import AccountSystem
+
+from sqlalchemy.orm.exc import NoResultFound
+
import fedmsgshim
from datetime import datetime, time
@@ -18,6 +48,16 @@ db = SQLAlchemy(app)
# set up FAS
fas = FAS(app)
+# FAS for usernames.
+# baseURL = "https://admin.fedoraproject.org/accounts"
+# fasuser = "elections"
+# faspw = "elections"
+# fas2_url = 'http://fakefas.fedoraproject.org/accounts/'
+fas2_url = app.config['FAS_BASE_URL']
+fas2_username = app.config['FAS_USERNAME']
+fas2_password = app.config['FAS_PASSWORD']
+fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password)
+
# modular imports
from fedora_elections import forms, models, redirect
@@ -55,20 +95,166 @@ def election_admin_required(f):
@app.route('/')
def index():
elections = models.Election.query.filter_by(frontpage=True).all()
- return flask.render_template('list/index.html', elections=elections)
+ return flask.render_template('list/index.html', elections=elections,
+ title="Elections")
+
+
+(a)app.route('/about/<election_alias>')
+def about_election(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/about.html', election=election,
+ usernamemap=usernamemap)
@app.route('/archive')
def archived_elections():
- raise NotImplementedError
+ now = datetime.utcnow()
+
+ try:
+ elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date<now, models.Election.id>0).all()
+ except NoResultFound:
+ flask.flash('There are no archived elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('election/archive.html', elections=elections)
+
+
+(a)app.route('/open')
+def open_elections():
+ now = datetime.utcnow()
+
+ try:
+ elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date>now,
+ models.Election.id>0).all()
+ except NoResultFound:
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ num_elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date>now,
+ models.Election.id>0).count()
+ if (num_elections == 0):
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('list/index.html', elections=elections,
+ title='Open Elections')
### ELECTION VIEWS #############################################
-(a)app.route('/vote/<election_alias>')
+(a)app.route('/vote/<election_alias>', methods=['GET', 'POST'])
def vote(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).one()
- return flask.render_template('election/vote.html', election=election)
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'Pending':
+ flask.flash('Voting has not yet started, sorry.')
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Ended':
+ flask.flash('This election is closed. You have been redirected to ' +
+ 'the election results.')
+ return flask.redirect(flask.url_for('results', election.shortname))
+
+ votes = models.Vote.query.filter_by(election_id=election.id,
+ voter=flask.g.fas_user.username).count()
+ if (votes != 0):
+ flask.flash('You have already voted in the election!')
+ return redirect.safe_redirect_back()
+
+ num_candidates = election.candidates.count()
+
+ uvotes = {}
+ for candidate in election.candidates:
+ uvotes[candidate.id] = ""
+ next_action = 'vote'
+
+ if flask.request.method == 'POST':
+ next_action = ''
+ form_values = flask.request.form.values()
+ if 'Submit' in form_values:
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote >= 0 and vote <= num_candidates:
+ uvotes[candidate.id] = int(vote)
+ else:
+ flask.flash("Invalid Ballot!")
+ return redirect.safe_redirect_back()
+
+ except ValueError:
+ flask.flash("Invalid Ballot!")
+ return redirect.safe_redirect_back()
+
+ for uvote in uvotes:
+ new_vote = models.Vote(election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=uvote,
+ value=uvotes[uvote] )
+ db.session.add(new_vote)
+ db.session.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return redirect.safe_redirect_back()
+
+ elif 'Preview' in form_values:
+ flask.flash("Please confirm your vote!")
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote > num_candidates:
+ flask.flash("Invalid data.")
+ uvotes[candidate.id] = 0
+ next_action = 'vote'
+ elif vote >= 0:
+ uvotes[candidate.id] = vote
+ else:
+ flask.flash("Invalid data2.")
+ uvotes[c.id] = 0
+ next_action = 'vote'
+ except ValueError:
+ flask.flash("Invalid data")
+
+ if (next_action != 'vote'):
+ next_action = 'confirm'
+
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/vote.html', election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ voteinfo=uvotes, nextaction=next_action)
### AUTH VIEWS #############################################
@@ -216,3 +402,54 @@ def admin_delete_candidate(election_alias, candidate_id):
election_alias=election.alias))
return flask.render_template('admin/delete_candidate.html', form=form,
candidate=candidate)
+
+
+(a)app.route('/results/<election_alias>')
+def election_results(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'In progress':
+ flask.flash("We are sorry. The results for this election cannot be" \
+ " viewed at this time because the election is still in" \
+ " progress.")
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Pending':
+ flask.flash("We are sorry. The results for this election cannot be" \
+ " viewed at this time because the election has not" \
+ " started.")
+ return redirect.safe_redirect_back()
+
+ elif election.embargoed == 1:
+ if identity.in_group(config.get('admingroup', 'elections')):
+ pass
+ else :
+ match = 0
+ admingroups = models.ElectionAdminGroups.query.filter_by(
+ election_id=election.id).all()
+ for group in admingroups:
+ if identity.in_group(group.group_name):
+ match = 1
+
+ if match == 0:
+ flask.flash("We are sorry. The results for this election" \
+ "cannot be viewed because they are currently" \
+ " embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/results.html', election=election,
+ usernamemap=usernamemap)
diff --git a/fedora_elections/images/border-left.png b/fedora_elections/images/border-left.png
new file mode 100644
index 0000000..6f256df
Binary files /dev/null and b/fedora_elections/images/border-left.png differ
diff --git a/fedora_elections/images/border-right.png b/fedora_elections/images/border-right.png
new file mode 100644
index 0000000..0e7365b
Binary files /dev/null and b/fedora_elections/images/border-right.png differ
diff --git a/fedora_elections/images/fedora-logo.png b/fedora_elections/images/fedora-logo.png
new file mode 100644
index 0000000..ce95893
Binary files /dev/null and b/fedora_elections/images/fedora-logo.png differ
diff --git a/fedora_elections/images/line.png b/fedora_elections/images/line.png
new file mode 100644
index 0000000..3498cb9
Binary files /dev/null and b/fedora_elections/images/line.png differ
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index f025255..994f144 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -2,7 +2,6 @@ from fedora_elections import app, db
from datetime import datetime
-
class Election(db.Model):
__tablename__ = 'elections'
@@ -63,6 +62,11 @@ class Candidate(db.Model):
# candidates_are_fasusers
url = db.Column(db.Unicode(250))
+ @property
+ def vote_count(self):
+ values = [v.value for v in self.votes]
+ return sum(values)
+
class LegalVoter(db.Model):
__tablename__ = 'legalvoters'
diff --git a/fedora_elections/static/fedora.css b/fedora_elections/static/fedora.css
new file mode 100644
index 0000000..e56f85f
--- /dev/null
+++ b/fedora_elections/static/fedora.css
@@ -0,0 +1,1334 @@
+*
+{
+ margin: 0;
+ padding: 0;
+}
+
+html, body
+{
+ height: 100%;
+}
+
+body
+{
+ font-size: 76%;
+ background: #FFFFFF url(../images/border-left.png) 0 0 repeat-y;
+}
+
+a img
+{
+ border: none;
+}
+
+pre
+{
+ font-size: 2.6ex;
+ overflow: auto;
+}
+
+abbr, acronym {
+ border-bottom: 1px dotted #000000;
+ cursor: help;
+}
+
+#wrapper
+{
+ font: normal 1em/1.5 sans-serif;
+ margin-left: 18px;
+ min-height: 100%;
+ background: #FFFFFF url(../images/border-right.png) 100% 0 repeat-y;
+ padding-right: 18px;
+ overflow: hidden;
+}
+
+#head
+{
+ position: relative;
+ border-top: 10px solid #337ACC;
+ padding: 1ex 2ex 17px;
+ background: #FFFFFF url(../images/line.png) 0 100% repeat-x;
+}
+
+#head h1 a
+{
+ display: block;
+ text-indent: -9999px;
+ background: url(../images/fedora-logo.png) 20px 50% no-repeat;
+ height: 73px;
+ width: 175px;
+ overflow: hidden;
+ text-decoration: none;
+}
+
+#sidebar
+{
+ float: left;
+ width: 180px;
+ margin: 2ex 0;
+ background: #FFFFFF;
+ padding-bottom: 120px;
+}
+
+#banner
+{
+ text-align: center;
+}
+
+#banner img
+{
+ display: block;
+ width: 200px;
+ margin-left: 10px;
+}
+
+#nav
+{
+ margin: 0 1.5ex;
+ padding: 0 1.5ex;
+}
+
+#nav h2
+{
+ font-size: 0.9em;
+ border-bottom: 1px dotted #AAAAAA;
+ color: #444444;
+ margin: 2ex 0 1ex;
+ text-transform: uppercase;
+ font-weight: normal;
+}
+
+#nav ul
+{
+ list-style: url(../images/arrow.png);
+ margin-left: 15px;
+}
+
+#nav li
+{
+ margin: 0.25ex 1ex;
+ color: #777777;
+ font-size: 0.9em;
+}
+
+#nav a
+{
+ font-weight: bold;
+ margin-right: 0.75ex;
+ color: #729FCF;
+ font-size: 1.1em;
+ display: block;
+}
+
+.home #nav-home a, .get #nav-get a, .join #nav-join a, .help #nav-help a
+{
+ color: #777777;
+ text-decoration: none;
+}
+
+#content
+{
+ display: inline-block;
+ margin-left: 230px;
+ padding: 1.5ex 2ex 140px 0;
+ margin-bottom: 3ex;
+ color: #666666;
+ background: #FFFFFF;
+}
+
+#content p, #content ul, #content ol
+{
+ font-size: 1em;
+}
+
+/* Blame IE6 and the guillotine bug for this one */
+#content { display: block; }
+
+#content h2
+{
+ font-size: 1.6em;
+ padding-top: .75em;
+}
+
+#content h3
+{
+ color: #337acc;
+ margin: 1.5ex 0 0.5ex;
+ font-size: 1.4em;
+}
+
+#content h3.header {
+ display: block;
+}
+
+#content table
+{
+ border-collapse: collapse;
+}
+
+#content table th
+{
+ background: #DDDDDD;
+}
+
+#content table th, #content table td
+{
+ border: 1px solid #000000;
+ padding: 0.6ex;
+}
+
+#content p
+{
+ margin: 1ex 5ex 1ex 0;
+}
+
+#content ul, #content ol {
+ margin: 0 3ex 2ex;
+}
+
+#content ul
+{
+ list-style: square;
+}
+
+#content img
+{
+ margin: 2ex 0;
+}
+
+
+
+#content .download
+{
+ list-style-image: url(../images/arrow.png);
+ list-style-position: outside;
+ margin: 1.6ex 3ex;
+}
+
+#content .download li
+{
+ color: #777777;
+ font-size: 1.1em;
+ background: #EFEFEF;
+ padding: 1ex;
+ border-bottom: 1px dotted #AAAAAA;
+}
+
+#content .download a
+{
+ font-weight: bold;
+}
+
+#content .roles
+{
+ margin: 0!important;
+ list-style: none!important;
+ display: inline;
+}
+
+#content .roles li
+{
+ float: left;
+ width: 130px;
+ margin-bottom: 0.5ex;
+}
+
+#content .roles a
+{
+ line-height: 1;
+ height: 150px;
+ text-align: center;
+ display: block;
+ text-decoration: none;
+}
+
+#content .roles a:hover
+{
+ background-color: #EFEFEF;
+}
+
+#footer
+{
+ position: relative;
+ font: normal 0.8em/1.5 sans-serif;
+ text-align: center;
+ background: white url(../images/line-bottom.png) 0 0 repeat-x;
+ margin: -140px 0 0;
+ border-bottom: 10px solid #337ACC;
+ height: 130px;
+ color: #4D4D4D;
+}
+
+#bottom
+{
+ margin-left: 18px;
+ padding-right: 18px;
+}
+
+#footer ul
+{
+ list-style: none;
+ margin-bottom: .8em;
+}
+
+#footer li
+{
+ display: inline;
+ border-left: 1px solid #AAAAAA;
+ padding: 0 0.75ex;
+ margin-left: -1px;
+}
+
+#footer li.first
+{
+ border-left: none;
+}
+
+#footer a
+{
+ color: #3771C8;
+}
+
+#footer a:hover
+{
+ color: #112233;
+}
+
+#footer p
+{
+ margin: 0 1.5ex;
+}
+
+#footer .copy
+{
+}
+
+#footer .rh-logo {
+ padding-top: 3ex;
+ margin-bottom: 1em;
+}
+s
+#screenshot-banner {
+ text-align: left;
+}
+
+input, textarea, select
+{
+ border: #AAA 1px solid;
+}
+
+input, textarea, option
+{
+ padding: 2px 5px 2px 5px;
+}
+
+#content #sponsors
+{
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ overflow: auto;
+}
+
+#content #sponsors li
+{
+ text-align: center;
+ float: left;
+ height: 100px;
+ margin: 0 1ex;
+}
+
+#content #sponsors li img
+{
+ display: block;
+}
+
+#content #primary-sponsors
+{
+ margin: 0 0 4ex;
+ padding: 0;
+ overflow: auto;
+ border-bottom: 1px #AAAAAA solid;
+}
+
+#content #primary-sponsors .psponsor
+{
+ text-align: center;
+ margin: 0 1ex;
+}
+
+#content #primary-sponsors .psponsor td
+{
+ border-style: none;
+}
+
+#content #primary-sponsors .psponsor img
+{
+ display: block;
+ float: left;
+ margin-right: 6ex;
+}
+
+#content #primary-sponsors .psponsor p
+{
+ display: block;
+ text-align: left;
+}
+
+#content ul.downloadbox
+{
+ overflow: hidden;
+ list-style: none;
+ margin: 0 2ex;
+ padding: 6ex 2ex 4ex 180px;
+ border-radius: 1.5ex;
+ -webkit-border-radius: 1.5ex;
+ -moz-border-radius: 1.5ex;
+ background: #C0DBDD 30px 50% no-repeat;
+ font-size: 0.9em;
+ display: inline-block;
+}
+
+#content ul.installdvd { background-image: url(../images/installdvd.png); }
+#content ul.gnomelive { background-image: url(../images/gnomelive.png); }
+#content ul.kdelive { background-image: url(../images/kdelive.png); }
+
+.downloadbox li
+{
+ width: 32ex;
+ float: left;
+ padding: 0 0 2ex;
+}
+
+.downloadbox li li
+{
+ width: auto;
+ float: none;
+ padding: 0;
+}
+
+
+#content pre.command
+{
+ font-size: 1.3em;
+ border: solid #DDDDDD;
+ border-width: 1px 1px 1px 5px;
+ background: #F2F5F6;
+ padding: 0.5ex 2ex;
+}
+
+#content p.mini-note
+{
+ padding: 1ex 2ex;
+ border: 1px solid #DDDDDD;
+ background: #FFABAB;
+ font-size: .8em;
+ color: #4c4c4c;
+ -webkit-border-radius: 1.5ex;
+ -moz-border-radius: 1.5ex;
+ width: 80%
+}
+
+#content p.note
+{
+ padding: 1ex 2ex;
+ border: 1px solid #DDDDDD;
+ background: #F2F5F6;
+}
+
+#content p.warning
+{
+ padding: 1ex 2ex;
+ border: 1px solid #DDDDDD;
+ background: #FF6666;
+ color: #000000;
+}
+
+#content p.warning a
+{
+ color: #00154F;
+}
+#content ul#resources
+{
+ list-style: none;
+}
+
+#content ul#resources a
+{
+ padding-left: 20px;
+ background: url(../images/arrow.png) 0 50% no-repeat;
+}
+
+#content ul#resources li
+{
+ padding-left: 70px;
+ margin-bottom: 3.5ex;
+}
+
+ul#resources li.resource-docs
+{
+ background: url(../images/icon-docs.png) 0 50% no-repeat;
+}
+
+ul#resources li.resource-communicate
+{
+ background: url(../images/icon-communicate.png) 0 50% no-repeat;
+}
+
+ul#resources li.resource-download
+{
+ background: url(../images/icon-download.png) 0 50% no-repeat;
+}
+
+#content img.smirk
+{
+ margin: 0 1ex 0 0;
+}
+
+#content p.joinlink
+{
+ border: 1px solid #DDDDDD;
+ background: #EEEEEE;
+ padding: 1ex 5ex 1ex 5ex;
+ text-align: center;
+ font-size: 120%;
+ font-weight: bold;
+}
+
+#content #subprojects
+{
+ margin-right: 5ex;
+ padding-top: 1ex;
+}
+
+#content a:link
+{
+ color: #337ACC;
+ text-decoration: none;
+}
+
+#content a:visited
+{
+ color: #729FCF;
+ text-decoration: none;
+}
+
+/* get-fedora styles */
+
+.download-block .button:hover {
+ background-color: #9ECFFF !important;
+}
+
+.download-block {
+ margin-right: 23em;
+}
+
+.download-sidebar {
+ float: right;
+ width: 21em;
+ margin-left: 2em;
+}
+
+.download-sidebar a {
+ font-size: 1.5em;
+ display: block;
+ padding: 1em;
+ margin: 1em 0;
+}
+
+#other {
+ margin-top: 2ex;
+ background-color: #F0F0F0!important;
+}
+
+.download-block h3 {
+ margin-top: 0!important;
+}
+
+
+.download-block em.byline {
+ display: block;
+ color: #999999;
+ font-size: 0.8em;
+}
+
+.download-block em.download-tagline {
+ color: #999999;
+ font-size: 0.8em;
+}
+
+.download-block .button {
+ border-radius: 5px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ color: #FFFFFF!important;
+ text-decoration: none;
+ font-weight: bold;
+ line-height: 3;
+ margin: 0;
+ padding: 0.4em 0.5em;
+ border: 1px solid #446382;
+ background-color: #5F94C9 !important;
+ margin-top: 0.9em;
+}
+
+.big-button {
+ border-radius: 5px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ color: #FFFFFF!important;
+ text-decoration: none;
+ font-weight: bold;
+ line-height: 3;
+ margin: 0;
+ padding: 0.4em 0.5em;
+ background-color: #337acc !important;
+ margin-top: 0.9em;
+ font-size: 1.7em;
+}
+
+.big-button:hover {
+ background-color: #295596 !important;
+}
+
+
+.fine-print {
+ font-size: .9em !important;
+ color: #999999;
+ font-style: italic;
+}
+
+.resources-pane {
+ float: right;
+ background-color: #E5E5E5;
+ padding: 0.5em 1em;
+ border-radius: 5px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ margin-left: 1em;
+}
+
+.download-block p img {
+ float: left;
+ margin-right: 1.4em !important;
+}
+
+/** sidebar boxes for get-fedora **/
+
+.download-sidebar a {
+ display: block;
+ background: #337ACC url(../images/gradient_bg.png) repeat-x;
+ border-radius: 8px;
+ -webkit-border-radius: 8px;
+ -moz-border-radius: 8px;
+}
+
+.download-sidebar a.even {
+ background-color: #1855A2;
+}
+
+.download-sidebar a:hover {
+ background-color: #1158AA;
+}
+
+.download-sidebar a.even:hover {
+ background-color: #003380;
+}
+
+.download-sidebar a {
+ color: #FFFFFF!important;
+ font-size: medium;
+ text-decoration: none;
+ font-weight: bold;
+ line-height: 1.5;
+}
+
+.download-sidebar span#thereismorefedora a {
+ display: block;
+ background: none;
+ margin: 0px;
+ padding: 0px;
+}
+
+.download-sidebar span#thereismorefedora img {
+ margin: 0px;
+ padding: 0px;
+}
+
+#breadcrumbs {
+ border-bottom: 1px solid #DDDDDD;
+ margin-bottom: 1ex;
+ margin-right: 24%;
+}
+
+#breadcrumbs a {
+ text-decoration: none;
+ font-size: 1em;
+ color: #7788DD;
+}
+
+/** Web App Styles **/
+/* Links to other web apps */
+a.bodhi {
+ padding-left: 20px;
+ background: url(../images/icons/fedora-infra-icon_bodhi.png) 0 50% no-repeat;
+}
+
+a.koji {
+ padding-left: 20px;
+ background: url(../images/icons/fedora-infra-icon_koji.png) 0 50% no-repeat;
+}
+
+a.bugz {
+ padding-left: 20px;
+ background: url(../images/icons/fedora-infra-icon_bugzilla.png) 0 50% no-repeat;
+}
+
+a.pkgdb {
+ padding-left: 20px;
+ background: url(../images/icons/fedora-infra-icon_pkgdb.png) 0 50% no-repeat;
+}
+
+a.cvs {
+ padding-left: 20px;
+ background: url(../images/icons/fedora-infra-icon_source-control.png) 0 50% no-repeat;
+}
+
+/* tooltip */
+.tooltip {
+ display: block;
+ position: absolute;
+ border: 1px solid black;
+ background-color: #ffffdd;
+ width: 200px;
+}
+
+/* style to hide an element */
+.invisible {
+ display: none;
+}
+
+/* styles for tg flash messages */
+.flash {
+ background: #DEE681;
+ border: 1px solid #CCBBAA;
+ padding: 1.5ex 15px 1.5ex 43px;
+ margin: 1ex 0;
+}
+
+.flash .success {
+ background: #DEE681 url(../images/success.png) 10px 50% no-repeat;
+}
+
+.flash .warning {
+ background: #DEE681 url(../images/important.png) 10px 50% no-repeat;
+}
+
+.flash .info {
+ background: #DEE681 url(../images/info.png) 10px 50% no-repeat;
+}
+
+/* toolbar styles */
+.toolbar {
+ list-style: none;
+ list-style-image: none;
+ list-style-position: outside;
+ list-style-type: none;
+ text-align: right;
+ color: #888888;
+ font-size: 0.9em;
+ text-transform: uppercase;
+ vertical-align: middle;
+}
+
+.toolbar * {
+ display: inline;
+}
+
+#head #main-toolbar {
+ float: right;
+ margin-top: 9px;
+ width: 50%;
+}
+
+.toolbar input.button {
+ background-color: #888888;
+ color: white;
+ font-size: 0.9em;
+ margin-left: 5px;
+ padding: 2px;
+ text-transform: uppercase;
+ vertical-align: middle;
+}
+
+/* panel styles */
+#content .panel {
+ border: medium none;
+ margin-top: 12px;
+}
+
+#content .panel h3 {
+ -moz-border-radius-bottomleft: 0;
+ -moz-border-radius-bottomright: 0;
+ -moz-border-radius-topleft: 12px;
+ -moz-border-radius-topright: 12px;
+ background-color: #D2D2D2;
+ color: white;
+ font-size: 1em;
+ padding: 8px 6px 6px;
+ text-transform: uppercase;
+ margin: 0;
+}
+
+/* Login panel */
+#login-box form .field {
+ margin: 0 0 1ex;
+ overflow: hidden;
+ text-align: left;
+}
+
+#login-box .field label {
+ clear: left;
+ float:left;
+ padding: 0 2ex 0 0;
+ text-align: left;
+ width: 32ex;
+}
+
+#content .login {
+ -moz-border-radius-bottomleft: 10px;
+ -moz-border-radius-bottomright: 10px;
+ -moz-border-radius-topleft: 10px;
+ -moz-border-radius-topright: 10px;
+ background-color: #5F94C9;
+ color: white;
+ padding: 8px 12px;
+}
+
+#content div.login * {
+ color: white;
+ padding: 0;
+ margin: 0;
+}
+
+#content .login h3 {
+ background-color: #5F94C9;
+}
+
+#content .login input {
+ padding: 2px 5px;
+ margin-bottom: 5px;
+ color: black;
+}
+
+#content .login label {
+ color: white;
+ display: block;
+ float: left;
+ font-size: 0.8em;
+ margin-right: 10px;
+ width: 60px;
+ text-align: left;
+}
+
+#content .login .button {
+ background-color: #777777;
+ border: 1px solid #555555;
+ color: white;
+}
+
+#content .login ul {
+ list-style-position: inside;
+}
+
+#export-regulations {
+ padding-top: 1em;
+ margin-left: 2em;
+}
+
+#export-regulations .read-more img, #export-regulations .details img {
+ margin: 0;
+}
+
+#export-regulations .details {
+ display: inline;
+}
+
+#export-regulations a {
+ text-decoration: none;
+}
+
+.read-more, .re-collapse {
+ float:right;
+}
+
+.read-more img {
+ float: none;
+}
+
+/* applies to get-* pages */
+
+#download-resources {
+ float:right;
+ margin-left:.5em;
+ width:21em; /* tofix */
+}
+
+#download-resources h4 {
+ text-transform: uppercase;
+ margin-bottom: .5em;
+}
+
+#download-resources li {
+ padding: 3px 0;
+}
+
+#download-resources ul.checkmark-list {
+ list-style: disc url("/static/images/icon-checkmark.png") outside;
+}
+
+#download-resources li.guide-list {
+ list-style: disc url("/static/images/icon-guide.png") outside;
+}
+
+#download-resources li.webpage-list {
+ list-style: disc url("/static/images/icon-webpage.png") outside;
+}
+
+#download-resources li.support-list {
+ list-style: disc url("/static/images/icon-support.png") outside;
+}
+
+#download-resources li.verify-list {
+ list-style: disc url("/static/images/icon-signed-verified.png") outside;
+}
+
+#download-resources div, #download-resources div h4 {
+ padding: 5px 0 5px 0;
+}
+
+#download-resources div p {
+ padding-left: 5px;
+}
+
+#download-resources p,
+#download-resources ul {
+ margin-bottom: 2em;
+}
+
+#download-resources ul {
+ margin-left: 2em;
+}
+
+#download-options-area {
+ background-color: #f2f2f2;
+ padding: 15px;
+}
+
+#download-options-area h4 {
+ color: #337ACC;
+}
+
+.other-options {
+ float: right;
+ border-radius: 5px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ margin-left: 1em;
+ width: 45%;
+}
+
+.upgrade-section {
+ width: 50%;
+}
+
+/* get-* format tab */
+.format-arch {
+ border-bottom: 1px #E5E5E5 solid;
+ float:left;
+ width: 90%;
+ padding: 5px;
+}
+
+.support-icon {
+ border: 0px;
+ padding: 3px;
+}
+
+.vendor-header {
+ clear: left;
+ font-size: 1.3em;
+ padding-top: 15px;
+ padding-bottom: 15px;
+}
+
+.freemedia-header {
+ clear: left;
+ font-size: 1.3em;
+ padding-top: 15px;
+}
+
+
+.vendor1, .vendor2 {
+ width: 50%;
+}
+
+/* end: applies to get-* pages */
+
+/* tabs */
+/* root element for tabs */
+ul.tabs {
+ margin: 0 !important;
+ padding: 0;
+ height: 30px;
+ border-bottom: 1px solid #dcdcdc;
+ width: 70%;
+}
+
+/* single tab */
+ul.tabs li {
+ float: left;
+ padding: 0;
+ margin: 0;
+ list-style-type: none;
+}
+
+/* link inside the tab. no background image */
+ul.tabs a {
+ float: left;
+ font-size: 13px;
+ display: block;
+ padding: 5px 10px;
+ text-decoration: none;
+ border: 1px solid #dcdcdc;
+ border-bottom: 0px;
+ height: 18px;
+ background-color: #dcdcdc;
+ color: #3771c8;
+ margin-right: 4px;
+ -moz-border-radius-topleft: 4px;
+ -moz-border-radius-topright: 4px;
+ position: relative;
+ top: 1px;
+ text-transform: uppercase;
+ font-weight: bold;
+}
+
+ul.tabs a:hover {
+ background-color: #fff;
+ color: #818181;
+ text-transform: uppercase;
+ font-weight: bold;
+}
+
+/* selected tab */
+ul.tabs a.current {
+ background-color: #fff;
+ border-bottom: 2px solid #fff;
+ color: #808080;
+ cursor: default;
+ text-transform: uppercase;
+ font-weight: bold;
+}
+
+/* tab pane */
+div.panes div {
+ display: none;
+ border: 0px;
+ border-left: 1px solid #dcdcdc;
+ min-height: 150px;
+ padding: 15px 10px;
+ /* tofix width: 70%; */
+ float: left;
+}
+
+#content a.current:link {
+ color: #828282;
+}
+/* end: tabs */
+
+/* --- */
+#content span.download-opt1
+{
+ float: left;
+ width: 45%;
+}
+
+div.panes span.download-option {
+ display: block;
+ /* tofix: width: 300px !important; */
+ width: 50%;
+}
+
+div.panes span.opt1 { float: left; }
+div.panes span.opt2 {
+ float: right;
+ width: 50%;
+}
+
+#export-regulations {
+ clear: both;
+ margin-bottom: 2em;
+}
+
+/* Hack to make it simple to get rid of borders in a table (ianweller) */
+#content table.nobordersplz th, #content table.nobordersplz td { border-width: 0px; }
+
+/* get-option common */
+/* keeps common stuff in get pages */
+.star-fedora-default, .star-most-compatible {
+ background-color: #F7FF9D;
+ border: 1px solid #A19800;
+ padding: 0.2em;
+ font-weight: bold;
+ color: #A19800;
+ font-size: 0.8em;
+ margin: 7px;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+.download-button-mini {
+ border-radius: 5px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ color: #FFFFFF!important;
+ text-decoration: none;
+ font-weight: bold;
+ line-height: 2;
+ font-size:0.85em;
+ padding: 5px;
+ background-color: #337ACC !important;
+ white-space: nowrap;
+}
+
+.download-button-mini:hover {
+ background-color: #295596 !important;
+}
+
+.opt1 { float: left; }
+.opt2 { float: right; }
+
+#content .format-arch .opt1 p, #content .format-arch .opt1 h3, #content .format-arch .opt2 p, #content .format-arch .opt2 h3 {
+ padding: 0px;
+ margin: 0px
+}
+
+.panes {
+ width: 68%
+}
+
+.shownojs { display: none; }
+
+.area-callout
+{
+ display: inline-block; /* to put icons next to header */
+}
+
+/* end: get-option common */
+
+/* get-option - desktop pane */
+#content .desktop-section table, #content .desktop-section table td {
+ border: 0px;
+}
+
+.desktop-section ul {
+ float: left;
+}
+
+.desktop-section img.desktop-screenshot {
+ float: right;
+}
+
+.desktop-section table tbody tr {
+ background-color: #fff;
+}
+
+.desktop-section td.description {
+ max-width: 230px;
+}
+
+.desktop-section td {
+ vertical-align: top;
+}
+
+img.desktop-screenshot:hover {
+ cursor: pointer;
+ cursor: hand;
+}
+
+/* end: get-option - desktop pane */
+
+
+/* get-option - architecture pane */
+.arch-section, .desktop-section {
+ clear:left;
+ display:block;
+ margin-bottom:1em;
+ padding:0.5em 1em;
+}
+
+img.download-media
+{
+ float: left;
+ padding-right: 7px;
+}
+
+.arch-sparc-area {
+ background-color: #f5f5f5;
+ padding: 1em;
+ clear: both;
+ display: block;
+}
+
+.arch-section h3 span {
+ color: black;
+}
+
+/* end: get-option - architecture pane */
+
+.panes table tr {
+ background-color:#F2F2F2;
+ border:0 none;
+}
+
+#get-acitivites td.rank {
+ background-color:white;
+ font-weight:bold;
+ padding-left:0;
+ padding-right:7px;
+ text-align:left;
+}
+
+#get-acitivites td.name {
+ font-size:xx-small;
+ font-style:italic;
+}
+
+#get-acitivites table td {
+ border-color: white !important;
+ border-style: solid !important;
+ border-width: 0 0 18px !important;
+ padding: 0.8em;
+ vertical-align: top;
+}
+
+.learn-more {
+ display: none;
+}
+
+/*
+ * note: probably not needed
+#content table th, #content table td {
+ border:1px solid #000000;
+ padding:0.6ex;
+}
+*/
+
+/* end: get-option - activites pane */
+
+#content img {
+ margin:0;
+}
+
+
+
+/* make it work in 1024x786 */
+
+#desktop-versions {
+ width: 100%;
+}
+
+.panes {
+ min-width: 50%;
+}
+
+.panes table {
+ width: 80%;
+}
+
+#download-resources {
+ min-width: 200px;
+ width: 20%;
+}
+
+
+#content .panes {
+ min-width: 400px;
+}
+
+#nav {
+ margin: 0 1em;
+ padding:0 0 0 1.5em;
+}
+
+tr.dl-buttons td {
+ min-width: 104px !important;
+}
+
+div.panes div {
+ background-color: transparent !important;
+ padding-right: 15 5 15 10;
+}
+
+.arch-section,
+.desktop-section {
+ padding-right: 0.5em 0 0.5em 1em;
+}
+
+/* the overlayed element */
+.simple_overlay {
+
+ /* must be initially hidden */
+ display:none;
+
+ /* place overlay on top of other elements */
+ z-index:10000;
+
+ /* styling */
+ background-color:#333;
+
+ /* disabled for now - so it fits around images
+ * width:675px;
+ */
+ min-height:200px;
+ border:1px solid #666;
+
+ /* CSS3 styling for latest browsers */
+ -moz-box-shadow:0 0 90px 5px #000;
+ -webkit-box-shadow: 0 0 90px #000;
+}
+
+/* close button positioned on upper right corner */
+.simple_overlay .close {
+ background-image:url(../images/overlay-close.png);
+ position:absolute;
+ right:-15px;
+ top:-15px;
+ cursor:pointer;
+ height:35px;
+ width:35px;
+}
+
+/* styling for elements inside overlay */
+.overlay_details {
+ position:absolute;
+ top:15px;
+ right:15px;
+ font-size:11px;
+ color:#fff;
+ width:150px;
+}
+
+.overlay_details h3 {
+ color:#aba;
+ font-size:15px;
+ margin:0 0 -10px 0;
+}
+
+/* end: the overlayed element */
+
+.global-notice {
+ background-color: #f6f6f6;
+ border: 1px solid #ddd;
+ padding: 0.5em 1em;
+}
diff --git a/fedora_elections/static/style.css b/fedora_elections/static/style.css
index b316ea7..8c4a9b3 100644
--- a/fedora_elections/static/style.css
+++ b/fedora_elections/static/style.css
@@ -1,90 +1,144 @@
body {
- font-family: sans-serif;
- font-size: 12pt;
- line-height: 1.35;
+ font-family: sans-serif;
+ font-size: 12pt;
+ line-height: 1.35;
}
#flashes {
- background-color: #ffa;
+ background-color: #ffa;
}
nav {
- background-color: #eee;
- padding: 0.5em 1em;
+ background-color: #eee;
+ padding: 0.5em 1em;
}
nav h2 {
- display: inline;
- font-size: inherit;
- margin-right: 1em;
+ display: inline;
+ font-size: inherit;
+ margin-right: 1em;
}
nav ul {
- display: inline;
- list-style: none;
- padding: 0;
+ display: inline;
+ list-style: none;
+ padding: 0;
}
nav li {
- display: inline;
- margin-right: .5em;
+ display: inline;
+ margin-right: .5em;
}
.clear {
- clear: both;
+ clear: both;
}
/* input elements */
fieldset {
- border: 0;
- padding: 0;
- margin: .5em 0;
- float: left;
- clear: both;
+ border: 0;
+ padding: 0;
+ margin: .5em 0;
+ float: left;
+ clear: both;
}
.row {
- margin: .25em 0;
+ margin: .25em 0;
}
.row label:first-child {
- width: 8em;
- margin-right: 0.75em;
- text-align: right;
- clear: both;
- font-weight: bold;
- display: inline-block;
- vertical-align: top;
+ width: 8em;
+ margin-right: 0.75em;
+ text-align: right;
+ clear: both;
+ font-weight: bold;
+ display: inline-block;
+ vertical-align: top;
}
.row textarea {
- width: 30em;
- height: 8em;
+ width: 30em;
+ height: 8em;
}
form .row .errors {
- list-style: none;
- margin: .5em 0 1em 10.9375em;
- padding: 0;
- font-size: .8em;
- color: #c00;
- width: 30em;
+ list-style: none;
+ margin: .5em 0 1em 10.9375em;
+ padding: 0;
+ font-size: .8em;
+ color: #c00;
+ width: 30em;
}
div.help {
- width: 40%;
- float: right;
- background-color: #ffa;
- border-top: 1px solid #ee9;
- padding: 1em;
- font-size: .8em;
- margin: .625em 0;
+ width: 40%;
+ float: right;
+ background-color: #ffa;
+ border-top: 1px solid #ee9;
+ padding: 1em;
+ font-size: .8em;
+ margin: .625em 0;
}
div > *:first-child { margin-top: 0; }
div > *:last-child { margin-bottom: 0; }
form .buttons.indent {
- margin-left: 8.75em;
+ margin-left: 8.75em;
+}
+
+.vthot
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #9C0000;
+}
+
+.vtwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #FFA300;
+}
+
+.vtmedwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #93FF63;
+}
+
+.vtmedcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #00DAFE;
+}
+
+.vtcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #0033FF;
+}
+
+.vtcold
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #000083;
}
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index e40f882..fafefee 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -1,21 +1,26 @@
{% extends "base.html" %}
{% block title %}Election administration{% endblock %}
{% block body %}
-<h2>Election administration</h2>
-<table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- </tr>
- {% for election in elections %}
- <tr>
- <td><a href="{{ url_for('admin_view_election', election_alias=election.alias) }}">{{ election.summary }}</a></td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- </tr>
- {% endfor %}
-</table>
-{% endblock %}
+ <h2>Election administration</h2>
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>
+ <a href="{{ url_for('admin_view_election',
+ election_alias=election.alias) }}">
+ {{ election.summary }}
+ </a>
+ </td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+{%endblock %}
diff --git a/fedora_elections/templates/admin/candidate.html b/fedora_elections/templates/admin/candidate.html
index 9546da0..a7b2fda 100644
--- a/fedora_elections/templates/admin/candidate.html
+++ b/fedora_elections/templates/admin/candidate.html
@@ -2,16 +2,18 @@
{% from "_formhelpers.html" import render_field %}
{% block title %}{{ submit_text }}{% endblock %}
{% block body %}
-<h2>{{ submit_text }}</h2>
+ <h2>{{ submit_text }}</h2>
-<form action="" method="post">
- <fieldset>
- {{ render_field(form.name) }}
- {{ render_field(form.url) }}
- </fieldset>
+ <form action="" method="post">
+ <fieldset>
+ {{ render_field(form.name) }}
+ {{ render_field(form.url) }}
+ </fieldset>
- <br class="clear">
- <p class="buttons"><input type="submit" class="submit positive" value="{{ submit_text }}"></p>
- {{ form.csrf_token }}
-</form>
+ <br class="clear">
+ <p class="buttons">
+ <input type="submit" class="submit positive" value="{{ submit_text }}">
+ </p>
+ {{ form.csrf_token }}
+ </form>
{% endblock %}
diff --git a/fedora_elections/templates/admin/delete_candidate.html b/fedora_elections/templates/admin/delete_candidate.html
index 3cfe4be..d764f19 100644
--- a/fedora_elections/templates/admin/delete_candidate.html
+++ b/fedora_elections/templates/admin/delete_candidate.html
@@ -2,12 +2,17 @@
{% from "_formhelpers.html" import render_field %}
{% block title %}Delete candidate{% endblock %}
{% block body %}
-<h2>{{ self.title() }}</h2>
+ <h2>{{ self.title() }}</h2>
-<form action="" method="post">
- <p>Are you sure you want to delete candidate "{{ candidate.name }}"?</p>
- <p class="buttons"><input type="submit" value="Delete candidate">
- <a href="{{ url_for('admin_view_election', election_alias=candidate.election.alias) }}">Cancel</a></p>
- {{ form.csrf_token }}
-</form>
+ <form action="" method="post">
+ <p>Are you sure you want to delete candidate "{{ candidate.name }}"?</p>
+ <p class="buttons">
+ <input type="submit" value="Delete candidate">
+ <a href="{{ url_for('admin_view_election',
+ election_alias=candidate.election.alias) }}">
+ Cancel
+ </a>
+ </p>
+ {{ form.csrf_token }}
+ </form>
{% endblock %}
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index e9d05c7..6e2a3d6 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -2,61 +2,76 @@
{% from "_formhelpers.html" import render_field %}
{% block title %}{{ submit_text }}{% endblock %}
{% block body %}
-<h2>{{ self.title() }}</h2>
+ <h2>{{ self.title() }}</h2>
-<form action="" method="post">
- <fieldset>
- {{ render_field(form.summary) }}
- </fieldset>
- <div class="help">
- <p>This is the name of the election that users will first see.</p>
- <p>Example: <kbd>Fedora Board, November 2010</kbd></p>
- </div>
- <fieldset>
- {{ render_field(form.alias) }}
- </fieldset>
- <div class="help">
- <p>A short, unique name that will be used for the election's URL. Lowercase letters, numbers, hyphens and underscores only.</p>
- <p>Example: <kbd>boardf15</kbd></p>
- </div>
- <fieldset>
- {{ render_field(form.description) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.url) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.start_date, " at 00:00:00 UTC") }}
- {{ render_field(form.end_date, " at 23:59:59 UTC") }}
- </fieldset>
- <div class="help">
- <p>Enter dates in <kbd>YYYY-MM-DD</kbd> format.</p>
- </div>
- <fieldset>
- {{ render_field(form.number_elected) }}
- </fieldset>
- <div class="help">
- <p>This determines the cutoff for how many candidates win the election. For example, if you are electing four empty seats on a committee, enter <kbd>4</kbd> here.</p>
- </div>
- <fieldset>
- {{ render_field(form.candidates_are_fasusers) }}
- </fieldset>
- <div class="help">
- <p>Check this if every candidate in this election has a FAS user account. This will automatically pull full names and wiki page URLs from FAS.</p>
- </div>
- <fieldset>
- {{ render_field(form.frontpage) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.embargoed) }}
- </fieldset>
- <div class="help">
- <p>Embargo results when the election is finished. To unembargo them, edit the election and uncheck this box.</p>
- </div>
+ <form action="" method="post">
+ <fieldset>
+ {{ render_field(form.summary) }}
+ </fieldset>
+ <div class="help">
+ <p>This is the name of the election that users will first see.</p>
+ <p>Example: <kbd>Fedora Board, November 2010</kbd></p>
+ </div>
+ <fieldset>
+ {{ render_field(form.alias) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ A short, unique name that will be used for the election's URL.
+ Lowercase letters, numbers, hyphens and underscores only.
+ </p>
+ <p>Example: <kbd>boardf15</kbd></p>
+ </div>
+ <fieldset>
+ {{ render_field(form.description) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.url) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.start_date, " at 00:00:00 UTC") }}
+ {{ render_field(form.end_date, " at 23:59:59 UTC") }}
+ </fieldset>
+ <div class="help">
+ <p>Enter dates in <kbd>YYYY-MM-DD</kbd> format.</p>
+ </div>
+ <fieldset>
+ {{ render_field(form.number_elected) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ This determines the cutoff for how many candidates win the election.
+ For example, if you are electing four empty seats on a committee,
+ enter <kbd>4</kbd> here.
+ </p>
+ </div>
+ <fieldset>
+ {{ render_field(form.candidates_are_fasusers) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ Check this if every candidate in this election has a FAS user account.
+ This will automatically pull full names and wiki page URLs from FAS.
+ </p>
+ </div>
+ <fieldset>
+ {{ render_field(form.frontpage) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.embargoed) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ Embargo results when the election is finished. To unembargo them, edit
+ the election and uncheck this box.
+ </p>
+ </div>
- <br class="clear">
- {% block election_form_extra %}{% endblock %}
- <p class="buttons"><input type="submit" class="submit positive" value="{{ submit_text }}"></p>
- {{ form.csrf_token }}
-</form>
+ <br class="clear">
+ {% block election_form_extra %}{% endblock %}
+ <p class="buttons">
+ <input type="submit" class="submit positive" value="Save Changes">
+ </p>
+ {{ form.csrf_token }}
+ </form>
{% endblock %}
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index 4b5b174..ed070f3 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -1,26 +1,42 @@
{% extends "base.html" %}
{% block body %}
-<h2>{{ election.summary }}</h2>
-<p>{{ election.description }}</p>
-<p><a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">Edit election</a>
-<ul>
- <li>On front page: {{ election.frontpage }}</li>
- <li>Start date: {{ election.start_date }}</li>
- <li>End date: {{ election.end_date }}</li>
- <li>Number elected: {{ election.number_elected }}</li>
- <li>Election type: {{ election.voting_type }}</li>
-</ul>
-<h3>Candidates</h3>
-{% if election.candidates.all() %}
-<ul>
- {% for candidate in election.candidates.all() %}
- <li>{{ candidate.name }}
- <a href="{{ url_for('admin_edit_candidate', election_alias=election.alias, candidate_id=candidate.id) }}">[edit]</a>
- <a href="{{ url_for('admin_delete_candidate', election_alias=election.alias, candidate_id=candidate.id) }}">[x]</a></li>
- {% endfor %}
-</ul>
-{% else %}
-<p>There are no candidates.</p>
-{% endif %}
-<a href="{{ url_for('admin_add_candidate', election_alias=election.alias) }}">Add a candidate</a>
+ <h2>{{ election.summary }}</h2>
+ <p>{{ election.description }}</p>
+ <p>
+ <a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">
+ Edit election
+ </a>
+ </p>
+ <ul>
+ <li>On front page: {{ election.frontpage }}</li>
+ <li>Start date: {{ election.start_date }}</li>
+ <li>End date: {{ election.end_date }}</li>
+ <li>Number elected: {{ election.number_elected }}</li>
+ <li>Election type: {{ election.voting_type }}</li>
+ </ul>
+ <h3>Candidates</h3>
+ {% if election.candidates.all() %}
+ <ul>
+ {% for candidate in election.candidates.all() %}
+ <li>{{ candidate.name }}
+ <a href="{{ url_for('admin_edit_candidate',
+ election_alias=election.alias,
+ candidate_id=candidate.id) }}">
+ [edit]
+ </a>
+ <a href="{{ url_for('admin_delete_candidate',
+ election_alias=election.alias,
+ candidate_id=candidate.id) }}">
+ [x]
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>There are no candidates.</p>
+ {% endif %}
+ <a href="{{ url_for('admin_add_candidate',
+ election_alias=election.alias) }}">
+ Add a candidate
+ </a>
{% endblock %}
diff --git a/fedora_elections/templates/auth/login.html b/fedora_elections/templates/auth/login.html
index aafe3a8..9e7b73c 100644
--- a/fedora_elections/templates/auth/login.html
+++ b/fedora_elections/templates/auth/login.html
@@ -2,13 +2,19 @@
{% from "_formhelpers.html" import render_field %}
{% block title %}Log in{% endblock %}
{% block body %}
-<h2>Log in</h2>
+ <h2>Log in</h2>
-<form action="" method="post">
- <p>Log in using your <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a> account.</p>
- {{ render_field(form.username) }}
- {{ render_field(form.password) }}
- <p class="buttons indent"><input type="submit" class="submit positive" value="Log in"></p>
- {{ form.csrf_token }}
-</form>
+ <form action="" method="post">
+ <p>
+ Log in using your
+ <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a>
+ account.
+ </p>
+ {{ render_field(form.username) }}
+ {{ render_field(form.password) }}
+ <p class="buttons indent"><input type="submit" class="submit positive"
+ value="Log in">
+ </p>
+ {{ form.csrf_token }}
+ </form>
{% endblock %}
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
index b167ba4..3e6f806 100644
--- a/fedora_elections/templates/base.html
+++ b/fedora_elections/templates/base.html
@@ -1,42 +1,134 @@
<!DOCTYPE html>
<html>
- <head>
- {% block head %}
- <title>Fedora Elections{% if self.title() %} — {% block title %}{% endblock %}{% endif %}</title>
- <link rel="stylesheet" href="{{ url_for("static", filename="style.css") }}">
- {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
- {% endfor %}
- {% endblock %}
- </head>
- <body>
- <h1>Fedora Elections</h1>
- {% if g.fas_user %}
- <p>Hello, {{ g.fas_user.username }} — <a href="{{ url_for("auth_logout") }}">log out</a></p>
- {% else %}
- <p>You are not logged in — <a href="{{ url_for("auth_login") }}">log in</a></p>
- {% endif %}
- <nav>
- <h2>Elections</h2>
- <ul>
- <li><a href="{{ url_for("index") }}">Open elections</a></li>
- <li><a href="{{ url_for("archived_elections") }}">Archived elections</a></li>
- </ul>
- <br>
- <h2>Administration</h2>
- <ul>
- <li><a href="{{ url_for("admin_view_elections") }}">My elections</a></li>
- <li><a href="{{ url_for("admin_new_election") }}">Create new election</a></li>
- </ul>
- </nav>
- {% with messages = get_flashed_messages() %}
- {% if messages %}
- <ul id="flashes">
- {% for message in messages %}
- <li>{{ message }}</li>
- {% endfor %}
- </ul>
- {% endif %}
- {% endwith %}
- {% block body %}{% endblock %}
- </body>
+ <head>
+ {% block head %}
+ <title>
+ Fedora Elections
+ {% if self.title() %}
+ —
+ {% block title %}{% endblock %}
+ {% endif %}
+ </title>
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="{{ url_for("static", filename="style.css") }}" />
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="https://fedoraproject.org/static/css/fedora.css" />
+ {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
+ {% endfor %}
+ {% endblock %}
+ </head>
+ <body>
+ <div id="wrapper">
+ <div id="head">
+ <h1><a href="https://admin.fedoraproject.org/en/index">Fedora</a></h1>
+ </div>
+
+ <div id="sidebar">
+ <div id="nav">
+ <h2>Navigation</h2>
+ <ul>
+ <li id="nav-home">
+ <a href="http://fedoraproject.org/en/index">Home</a>
+ </li>
+ <li id="nav-get">
+ <a href="http://fedoraproject.org/en/get-fedora">Get Fedora</a>
+ </li>
+ <li id="nav-join">
+ <a href="http://fedoraproject.org/en/join-fedora">Join Fedora</a>
+ </li>
+ <li id="nav-help">
+ <a href="http://fedoraproject.org/en/get-help">Get Help</a>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <div id="content">
+ <ul class="toolbar" id="#main-toolbar">
+ {% if g.fas_user %}
+ <li>
+ Hello, {{ g.fas_user.username }} —
+ <a href="{{ url_for("auth_logout") }}">log out</a>
+ </li>
+ {% else %}
+ <li>
+ You are not logged in —
+ <a href="{{ url_for("auth_login") }}">log in</a>
+ </li>
+ {% endif %}
+ </ul>
+ <h2>Elections</h2>
+ <ul>
+ <li>
+ <a href="{{ url_for("open_elections") }}">
+ Open elections
+ </a>
+ </li>
+ <li>
+ <a href="{{ url_for("archived_elections") }}">
+ Archived elections
+ </a>
+ </li>
+ </ul>
+ <br>
+ <h2>Administration</h2>
+ <ul>
+ <li>
+ <a href="{{ url_for("admin_view_elections") }}">
+ My elections
+ </a>
+ </li>
+ <li>
+ <a href="{{ url_for("admin_new_election") }}">
+ Create new election
+ </a>
+ </li>
+ </ul>
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+ <ul id="flashes">
+ {% for message in messages %}
+ <li>{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {% endwith %}
+ {% block body %}
+ {% endblock %}
+ </div>
+ </div>
+ <div id="bottom">
+ <div id="footer">
+ {% block footer %}
+ <p class="copy">
+ Copyright © 2008 Red Hat, Inc. and others. Please send any
+ comments or corrections to the
+ <a href="mailto:webmaster@fedoraproject.org">
+ websites team
+ </a>.
+ </p>
+ <p class="disclaimer">
+ The Fedora Project is maintained and driven by the community and
+ sponsored by Red Hat. This is a community maintained site. Red
+ Hat is not responsible for content.
+ </p>
+ <ul>
+ <li class="first">
+ <a href="/en/sponsors">Sponsors</a>
+ </li>
+ <li>
+ <a href="http://fedoraproject.org/wiki/Legal">
+ Legal
+ </a>
+ </li>
+ <li>
+ <a href="http://fedoraproject.org/wiki/Legal/TrademarkGuidelines">
+ Trademark Guidelines
+ </a>
+ </li>
+ </ul>
+ </div>
+ {% endblock %}
+ </div>
+ </body>
</html>
diff --git a/fedora_elections/templates/election/about.html b/fedora_elections/templates/election/about.html
new file mode 100644
index 0000000..c7bf0ec
--- /dev/null
+++ b/fedora_elections/templates/election/about.html
@@ -0,0 +1,40 @@
+{% extends "base.html" %}
+{% block title %}Election Information{% endblock %}
+{% block body %}
+ <h1>{{ election.summary }}</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ {% if election.status == 'Ended' %}
+ <a href="/results/{{ election.alias }}">Show Results!</a>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p />
+ {% endif %}
+{% endblock %}
diff --git a/fedora_elections/templates/election/archive.html b/fedora_elections/templates/election/archive.html
new file mode 100644
index 0000000..ad35d6b
--- /dev/null
+++ b/fedora_elections/templates/election/archive.html
@@ -0,0 +1,28 @@
+{% extends "base.html" %}
+{% block body %}
+
+ <h2>Past Elections</h2>
+
+ <table border="1" cellpadding="1">
+ <tr>
+ <th></th>
+ <th>Start Date (UTC)</th>
+ <th>End Date (UTC)</th>
+ <th></th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{election.description}}</td>
+ <td>{{election.start_date}}</td>
+ <td>{{election.end_date}}</td>
+ <td>
+ <a href="about/{{ election.alias }}"">More information</a>
+ </td>
+ <td>
+ <a href="results/{{ election.alias }}">Results</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+{% endblock %}
diff --git a/fedora_elections/templates/election/results.html b/fedora_elections/templates/election/results.html
new file mode 100644
index 0000000..f61d214
--- /dev/null
+++ b/fedora_elections/templates/election/results.html
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+{% block body %}
+ <h1>{{ election.summary }} Results</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{candidate.name}}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ <td>{{ candidate.vote_count }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+{% endblock %}
diff --git a/fedora_elections/templates/election/vote.html b/fedora_elections/templates/election/vote.html
index 5f18efd..e3c4dd4 100644
--- a/fedora_elections/templates/election/vote.html
+++ b/fedora_elections/templates/election/vote.html
@@ -6,22 +6,111 @@
<p><a href="{{election.url}}">[More Information]</a></p>
-<table>
- {% for candidate in election.candidates %}
- <tr>
- <td>{{candidate.name}} <a href="{{candidate.url}}">[Info]</a></td>
- <!-- If user is logged in: -->
- <td><!-- Render the wtform for the candidate voting box here --></td>
- <!-- End if user logged in -->
- </tr>
- {% endfor %}
-</table>
+<form action="" method="post">
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ <a href="{{candidate.url}}">[Info]</a>
+ </td>
+ <!-- If user is logged in: -->
+ {% if g.fas_user %}
+ {% if nextaction == 'vote' %}
+ <td>
+ <select name="{{candidate.id}}">
+ {% for opt in range(0, num_candidates+1) %}
+ <option value={{opt}}>{{opt}}</option>
+ {% endfor %}
+ </select>
+ </td>
+ {% endif %}
+ {% if nextaction == 'confirm' %}
+ <td>
+ {% set cid = candidate.id %}
+ <input type="hidden" name="{{candidate.id}}"
+ value="{{voteinfo[cid]}}" />
+ {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
+ {% if weightpercent >= 90 %}
+ <div class="vthot">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 70 %}
+ <div class="vtwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 50 %}
+ <div class="vtmedwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 30 %}
+ <div class="vtmedcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 10 %}
+ <div class="vtcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% else %}
+ <div class="vtcold">
+ {{voteinfo[cid]}}
+ </div>
+ {% endif %}
+ </td>
+ {% endif %}
+ {% endif %}
+ <!-- End if user logged in -->
+ </tr>
+ {% endfor %}
+ </table>
+ {% if g.fas_user %}
+ <p>
+ {% if nextaction == 'confirm' %}
+ <input type="submit" name="{{nextaction}}" value="Submit" />
+ {% else %}
+ <input type="submit" name="{{nextaction}}" value="Preview" />
+ {% endif %}
+ </p>
+ {% endif %}
+</form>
<!-- Make this conditional on whether the user is logged in and can vote in the election -->
-To vote in this election you must be a member of any one of the following groups:
+{% if election.legal_voters.count() != 0 %}
+<p>
+ To vote in this election you must be a member of any one of the following groups:
+</p>
<ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
</ul>
+{% else %}
+<p />
+{% endif %}
+<div>
+ <h2>Information on Range Voting</h2>
+ <p>
+ Fedora Project has implemented
+ <a href="http://rangevoting.org/">Range Voting</a>
+ for this election, in particular the "Range (score-summing, blanks
+ treated as zero score, no quorum rule)" range voting system.
+ </p>
+ <p>
+ To cast your vote in this election simply select a value between 0 and
+ {{num_candidates}} with 0 as 'least or no preference' and
+ {{num_candidates}} as 'highest preference'.
+ </p>
+ <p>
+ At the end of the election, the highest ranking candidate(s) are marked
+ as the winners.
+ </p>
+ <p>
+ For more information about Range Voting, visit the
+ <a href="http://rangevoting.org/">Center for Range Voting</a>.
+ </p>
+</div>
{% endblock %}
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 90aafa1..2985fcc 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -1,26 +1,30 @@
{% extends "base.html" %}
{% block body %}
-<h2>Open elections</h2>
-<table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- <th></th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>{{ election.summary }}</td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- <td>
- {% if election.status == "In progress" %}
- <a href="{{ url_for('vote', election_alias=election.alias) }}">Vote now!</a>
- {% endif %}
- </td>
- </tr>
- {% endfor %}
-</table>
+ <h2>{{ title }}</h2>
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{ election.summary }}</td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ <td>
+ {% if election.status == "In progress" %}
+ {% if g.fas_user %}
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">
+ Vote now!
+ </a>
+ {% endif %}
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
{% endblock %}
diff --git a/runserver.py b/runserver.py
index 176e2b9..b9103e0 100644
--- a/runserver.py
+++ b/runserver.py
@@ -1,2 +1,6 @@
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7']
+import pkg_resources
+
from fedora_elections import app
-app.run()
+app.run(host='0.0.0.0', port=80)
commit b485bd715836ea03a380b539209e9083e17eae39
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 21:40:58 2012 -0400
Make fedmsg optional with a shim module.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index fb4fc87..78d2a16 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -2,7 +2,7 @@ import flask
from flask.ext.fas import FAS
from flask.ext.sqlalchemy import SQLAlchemy
-import fedmsg
+import fedmsgshim
from datetime import datetime, time
from functools import wraps
@@ -123,7 +123,7 @@ def admin_new_election():
db.session.add(admin)
db.session.commit()
flask.flash('Election "%s" added' % election.summary)
- fedmsg.publish(topic="election.new", msg=election)
+ fedmsgshim.publish(topic="election.new", msg=election)
return flask.redirect(flask.url_for('admin_view_election',
election_alias=election.alias))
return flask.render_template('admin/election_form.html', form=form,
diff --git a/fedora_elections/fedmsgshim.py b/fedora_elections/fedmsgshim.py
new file mode 100644
index 0000000..775f785
--- /dev/null
+++ b/fedora_elections/fedmsgshim.py
@@ -0,0 +1,17 @@
+""" This is a temporary shim that allows fedmsg to be an optional dependency of
+elections. If fedmsg is installed, these function calls will try to actually send
+messages. If it is not installed, it will return silently.
+
+ :Author: Ralph Bean <rbean(a)redhat.com>
+
+"""
+
+import warnings
+
+
+def publish(*args, **kwargs):
+ try:
+ import fedmsg
+ fedmsg.publish(*args, **kwargs)
+ except Exception, e:
+ warnings.warn(str(e))
commit b0557aa45becf446fc3e49a07904cad8e85f091d
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 21:28:21 2012 -0400
Revert "Add fedmsg to the list of requirements."
This reverts commit 471fabeecb4121b7837b7de509cee76385cfe8f8.
diff --git a/requirements.txt b/requirements.txt
index 36d8ca3..89a2349 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,4 +6,3 @@ python-fedora
Flask-FAS
Flask-sqlalchemy
Flask-wtf
-fedmsg>=0.3.9
commit 3eb4f4825a55d335b1b1c5d8eb3ee64eb1096e75
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 12:09:59 2012 -0400
Emit a fedmsg message when a new election is created.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 2983de0..fb4fc87 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -2,6 +2,8 @@ import flask
from flask.ext.fas import FAS
from flask.ext.sqlalchemy import SQLAlchemy
+import fedmsg
+
from datetime import datetime, time
from functools import wraps
from urlparse import urlparse, urljoin
@@ -121,6 +123,7 @@ def admin_new_election():
db.session.add(admin)
db.session.commit()
flask.flash('Election "%s" added' % election.summary)
+ fedmsg.publish(topic="election.new", msg=election)
return flask.redirect(flask.url_for('admin_view_election',
election_alias=election.alias))
return flask.render_template('admin/election_form.html', form=form,
commit 5133c9a4e57c419e66d2665f1ad8760c4ab65c8f
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 12:09:38 2012 -0400
Add fedmsg to the list of requirements.
diff --git a/requirements.txt b/requirements.txt
index 89a2349..36d8ca3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,3 +6,4 @@ python-fedora
Flask-FAS
Flask-sqlalchemy
Flask-wtf
+fedmsg>=0.3.9
commit 2fcaaa54405acc0117f345b9a90022dc684d2649
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 12:09:20 2012 -0400
Add a fedmsg development config.
diff --git a/fedmsg.d/base.py b/fedmsg.d/base.py
new file mode 100644
index 0000000..dc1d155
--- /dev/null
+++ b/fedmsg.d/base.py
@@ -0,0 +1,39 @@
+# This file is part of fedmsg.
+# Copyright (C) 2012 Red Hat, Inc.
+#
+# fedmsg is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# fedmsg is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with fedmsg; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Ralph Bean <rbean(a)redhat.com>
+#
+config = dict(
+ # Set this to dev if you're hacking on fedmsg or an app.
+ # Set to stg or prod if running in the Fedora Infrastructure
+ environment="dev",
+
+ # Default is 0
+ high_water_mark=0,
+ io_threads=1,
+
+ ## For the fedmsg-hub and fedmsg-relay. ##
+
+ # We almost always want the fedmsg-hub to be sending messages with zmq as
+ # opposed to amqp or stomp.
+ zmq_enabled=True,
+
+ # When subscribing to messages, we want to allow splats ('*') so we tell the
+ # hub to not be strict when comparing messages topics to subscription
+ # topics.
+ zmq_strict=False,
+)
diff --git a/fedmsg.d/endpoints.py b/fedmsg.d/endpoints.py
new file mode 100644
index 0000000..88b37c0
--- /dev/null
+++ b/fedmsg.d/endpoints.py
@@ -0,0 +1,44 @@
+# This file is part of fedmsg.
+# Copyright (C) 2012 Red Hat, Inc.
+#
+# fedmsg is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# fedmsg is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with fedmsg; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Ralph Bean <rbean(a)redhat.com>
+#
+import socket
+hostname = socket.gethostname()
+
+config = dict(
+ # This is a dict of possible addresses from which fedmsg can send
+ # messages. fedmsg.init(...) requires that a 'name' argument be passed
+ # to it which corresponds with one of the keys in this dict.
+ endpoints={
+ # These are here so your local box can listen to the upstream
+ # infrastructure's bus. Cool, right? :)
+ "fedora-infrastructure": [
+ "tcp://fedoraproject.org:9940",
+ "tcp://stg.fedoraproject.org:9940",
+ ],
+
+ # This is the output side of the relay to which all other
+ # services can listen.
+ "relay_outbound": ["tcp://*:4001"],
+
+ # And this is what our app actually uses.
+ "fedora_elections.%s" % hostname: ["tcp://*:3007"],
+ },
+
+ relay_inbound="tcp://127.0.0.1:2003",
+)
diff --git a/fedmsg.d/ssl.py b/fedmsg.d/ssl.py
new file mode 100644
index 0000000..a286c77
--- /dev/null
+++ b/fedmsg.d/ssl.py
@@ -0,0 +1,41 @@
+# This file is part of fedmsg.
+# Copyright (C) 2012 Red Hat, Inc.
+#
+# fedmsg is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# fedmsg is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with fedmsg; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Ralph Bean <rbean(a)redhat.com>
+#
+import os
+import socket
+
+SEP = os.path.sep
+here = os.getcwd()
+hostname = socket.gethostname()
+
+config = dict(
+ sign_messages=False,
+ validate_signatures=False,
+ ssldir="/etc/pki/fedmsg",
+
+ crl_location="https://fedoraproject.org/fedmsg/crl.pem",
+ crl_cache="/tmp/crl.pem",
+ crl_cache_expiry=10,
+
+ certnames={
+ # In prod/stg, map hostname to the name of the cert in ssldir.
+ # Unfortunately, we can't use socket.getfqdn()
+ #"app01.stg": "app01.stg.phx2.fedoraproject.org",
+ },
+)
commit 29548ee3daaa3a374a8ee65d92fb657a097e8c1b
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 12:08:52 2012 -0400
Move kitchen above python-fedora in the requirements.txt list.
diff --git a/requirements.txt b/requirements.txt
index f6f22bc..89a2349 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,8 @@
# Used for when working from a virtualenv.
# Use this file by running "$ pip install -r requirements.txt"
flask
-python-fedora
kitchen
+python-fedora
Flask-FAS
Flask-sqlalchemy
Flask-wtf
commit 4709e0f153c93e4a74432e805dc7e4ae76a452c5
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Tue Aug 28 08:35:35 2012 -0700
Rest of the changes to merge about into vote
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 5b4cd80..2983de0 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -63,12 +63,6 @@ def archived_elections():
### ELECTION VIEWS #############################################
-(a)app.route('/about/<election_alias>')
-def about(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).one()
- return flask.render_template('election/about.html', election=election)
-
-
@app.route('/vote/<election_alias>')
def vote(election_alias):
election = models.Election.query.filter_by(alias=election_alias).one()
diff --git a/fedora_elections/templates/election/vote.html b/fedora_elections/templates/election/vote.html
index f1e6a55..5f18efd 100644
--- a/fedora_elections/templates/election/vote.html
+++ b/fedora_elections/templates/election/vote.html
@@ -10,10 +10,14 @@
{% for candidate in election.candidates %}
<tr>
<td>{{candidate.name}} <a href="{{candidate.url}}">[Info]</a></td>
+ <!-- If user is logged in: -->
+ <td><!-- Render the wtform for the candidate voting box here --></td>
+ <!-- End if user logged in -->
</tr>
{% endfor %}
</table>
+<!-- Make this conditional on whether the user is logged in and can vote in the election -->
To vote in this election you must be a member of any one of the following groups:
<ul>
{% for group in election.legal_voters %}
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 8dc8fb4..90aafa1 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -17,7 +17,7 @@
<td>{{ election.status }}</td>
<td>
{% if election.status == "In progress" %}
- <a href="{{ url_for('about', election_alias=election.alias) }}">Vote now!</a>
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">Vote now!</a>
{% endif %}
</td>
</tr>
diff --git a/tests/base.py b/tests/base.py
index 2edf911..e12179b 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -22,7 +22,7 @@ class BaseRequestTests(object):
fedora_elections.db.drop_all()
url_subs = {'election_alias': None, 'candidate_id': None}
-anon_urls = ('/', '/archive', '/login', '/logout', '/about/%(election_alias)s')
+anon_urls = ('/', '/archive', '/login', '/logout', '/vote/%(election_alias)s')
authed_view_urls = ('/logout', '/vote/%(election_alias)s')
admin_view_urls = ('/admin', '/admin/new', '/admin/%(election_alias)s')
admin_modify_urls = ('/admin/new', '/admin/%(election_alias)s/edit',
commit 21951c7d86c0c691c1c7b645edc08e164d176805
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Tue Aug 28 08:34:47 2012 -0700
Merge the about page into the vote page
diff --git a/fedora_elections/templates/election/about.html b/fedora_elections/templates/election/about.html
deleted file mode 100644
index f1e6a55..0000000
--- a/fedora_elections/templates/election/about.html
+++ /dev/null
@@ -1,23 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-<h2>{{election.summary}}</h2>
-
-<p>{{election.description}}</p>
-
-<p><a href="{{election.url}}">[More Information]</a></p>
-
-<table>
- {% for candidate in election.candidates %}
- <tr>
- <td>{{candidate.name}} <a href="{{candidate.url}}">[Info]</a></td>
- </tr>
- {% endfor %}
-</table>
-
-To vote in this election you must be a member of any one of the following groups:
-<ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
-</ul>
-{% endblock %}
diff --git a/fedora_elections/templates/election/vote.html b/fedora_elections/templates/election/vote.html
index 90aafa1..f1e6a55 100644
--- a/fedora_elections/templates/election/vote.html
+++ b/fedora_elections/templates/election/vote.html
@@ -1,26 +1,23 @@
{% extends "base.html" %}
{% block body %}
-<h2>Open elections</h2>
+<h2>{{election.summary}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
<table>
+ {% for candidate in election.candidates %}
<tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- <th></th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>{{ election.summary }}</td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- <td>
- {% if election.status == "In progress" %}
- <a href="{{ url_for('vote', election_alias=election.alias) }}">Vote now!</a>
- {% endif %}
- </td>
+ <td>{{candidate.name}} <a href="{{candidate.url}}">[Info]</a></td>
</tr>
{% endfor %}
</table>
+
+To vote in this election you must be a member of any one of the following groups:
+<ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+</ul>
{% endblock %}
commit a34d75e1e9535e5d4bd4cd7685237e7888736dba
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 18:47:41 2012 -0700
Some more pages and functions that are blockers
diff --git a/TODO b/TODO
index 10e8e54..9858c8f 100644
--- a/TODO
+++ b/TODO
@@ -1,6 +1,9 @@
Things that need doing to get this deployed
===========================================
+* Add legalvoters to an election
+* Add page to vote
+* Add page that summarizes the vote results
* Write a conversion script for the old db model to the new one
* rpmify
* rpmify flask-fas (or get flask-fas into python-fedora)
commit 75db248833c113f3953b5dbb453386a3400f6b88
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 18:44:59 2012 -0700
Add an implementation of a page about an election
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index d59fa64..5b4cd80 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -61,6 +61,20 @@ def archived_elections():
raise NotImplementedError
+### ELECTION VIEWS #############################################
+
+(a)app.route('/about/<election_alias>')
+def about(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ return flask.render_template('election/about.html', election=election)
+
+
+(a)app.route('/vote/<election_alias>')
+def vote(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ return flask.render_template('election/vote.html', election=election)
+
+
### AUTH VIEWS #############################################
@app.route('/login', methods=('GET', 'POST'))
diff --git a/fedora_elections/templates/election/about.html b/fedora_elections/templates/election/about.html
new file mode 100644
index 0000000..f1e6a55
--- /dev/null
+++ b/fedora_elections/templates/election/about.html
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{election.summary}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
+<table>
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>{{candidate.name}} <a href="{{candidate.url}}">[Info]</a></td>
+ </tr>
+ {% endfor %}
+</table>
+
+To vote in this election you must be a member of any one of the following groups:
+<ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+</ul>
+{% endblock %}
diff --git a/fedora_elections/templates/election/vote.html b/fedora_elections/templates/election/vote.html
new file mode 100644
index 0000000..90aafa1
--- /dev/null
+++ b/fedora_elections/templates/election/vote.html
@@ -0,0 +1,26 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>Open elections</h2>
+<table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{ election.summary }}</td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ <td>
+ {% if election.status == "In progress" %}
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">Vote now!</a>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+</table>
+{% endblock %}
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 90aafa1..8dc8fb4 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -17,7 +17,7 @@
<td>{{ election.status }}</td>
<td>
{% if election.status == "In progress" %}
- <a href="{{ url_for('vote', election_alias=election.alias) }}">Vote now!</a>
+ <a href="{{ url_for('about', election_alias=election.alias) }}">Vote now!</a>
{% endif %}
</td>
</tr>
commit 48c4d127e65bf00c38a28052ccba67aeefb7c1dd
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 18:42:28 2012 -0700
Add two new pages for dsmoketesting
diff --git a/tests/base.py b/tests/base.py
index 0223c82..2edf911 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -22,8 +22,8 @@ class BaseRequestTests(object):
fedora_elections.db.drop_all()
url_subs = {'election_alias': None, 'candidate_id': None}
-anon_urls = ('/', '/archive', '/login', '/logout')
-authed_view_urls = ('/logout',)
+anon_urls = ('/', '/archive', '/login', '/logout', '/about/%(election_alias)s')
+authed_view_urls = ('/logout', '/vote/%(election_alias)s')
admin_view_urls = ('/admin', '/admin/new', '/admin/%(election_alias)s')
admin_modify_urls = ('/admin/new', '/admin/%(election_alias)s/edit',
'/admin/%(election_alias)s/candidates/new',
commit 671cf5562f4ad90c7d9eea652e4e7aa2d3d67db9
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 18:23:22 2012 -0700
A few more todo items: i18n, more paranoid error handling
diff --git a/TODO b/TODO
index 335107f..10e8e54 100644
--- a/TODO
+++ b/TODO
@@ -10,6 +10,8 @@ Things that need doing to get this deployed
us to make sure that no data changing requests are coded without wtf forms.
So this is really: audit to make sure everything is using wtf forms
+* Error handling -- unknown elections, etc
+
Future enhancements (anything not necessary for deploying)
==========================================================
@@ -26,3 +28,7 @@ Future enhancements (anything not necessary for deploying)
should go in a different module
*** return data to templating engine
*** Association with a URL
+* i18n. Need review: https://bugzilla.redhat.com/show_bug.cgi?id=839071
+** mark strings
+** Pull translatable strings out
+** set up transifex
commit d05250abdd86aec4534e8e839d487d719b17c33f
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 17:43:22 2012 -0700
Smoketesting now covers all urls' GET method.
diff --git a/tests/base.py b/tests/base.py
index 8271e65..0223c82 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -21,10 +21,11 @@ class BaseRequestTests(object):
fedora_elections.db.session.remove()
fedora_elections.db.drop_all()
+url_subs = {'election_alias': None, 'candidate_id': None}
anon_urls = ('/', '/archive', '/login', '/logout')
-admin_urls = ('/admin', '/admin/new', '/admin/%(election_alias)s',
- '/admin/%(election_alias)s/edit',
+authed_view_urls = ('/logout',)
+admin_view_urls = ('/admin', '/admin/new', '/admin/%(election_alias)s')
+admin_modify_urls = ('/admin/new', '/admin/%(election_alias)s/edit',
'/admin/%(election_alias)s/candidates/new',
'/admin/%(election_alias)s/candidates/%(candidate_id)d/edit',
'/admin/%(election_alias)s/candidates/%(candidate_id)d/delete')
-authed_urls = ('/logout',)
diff --git a/tests/test_smoke.py b/tests/test_smoke.py
index 6b03e8e..1d7b306 100644
--- a/tests/test_smoke.py
+++ b/tests/test_smoke.py
@@ -2,19 +2,42 @@
#
# Copyright (C) 2012 Red Hat, Inc.
#
-# Author(s): Toshio Kuratomi <toshio(a)fedoraproject.org>
+# Author(s): Toshio Kuratomi <toshio(a)fedoraproject.org>
+from copy import copy
+import datetime
from functools import partial
+from mock import Mock
from nose import tools, with_setup
-from base import BaseRequestTests, anon_urls, authed_urls, admin_urls
+from base import BaseRequestTests, anon_urls, authed_view_urls, \
+ admin_view_urls, admin_modify_urls, url_subs
+
+from fedora_elections import db, models
+from fedora_elections import fas
class TestSmoke(BaseRequestTests):
'''Basic test that we don't get 500s'''
def setUp(self):
super(TestSmoke, self).setUp()
+ fas.login = Mock(return_value=True)
+ # Add an election
+ election = models.Election(summary='Test Election 1',
+ alias='test1', description='lorem ipsum of course',
+ url='http://fedorahosted.org/elections',
+ start_date=datetime.datetime.utcnow(),
+ end_date=datetime.datetime.utcnow()+datetime.timedelta(hours=1))
+ db.session.add(election)
+ # Add candidate to election
+ candidate = models.Candidate(election=election, name='Mr Testy', url='http://fedoraproject.org')
+ db.session.add(candidate)
+ db.session.commit()
+
+ self.url_subs = copy(url_subs)
+ self.url_subs['election_alias'] = 'test1'
+ self.url_subs['candidate_id'] = candidate.id
def tearDown(self):
super(TestSmoke, self).tearDown()
@@ -29,7 +52,7 @@ class TestSmoke(BaseRequestTests):
return self.app.get('/logout', follow_redirects=True)
def check_status_code(self, url, status_codes=[200]):
- resp = self.app.get(url, follow_redirects=True)
+ resp = self.app.get(url % self.url_subs, follow_redirects=True)
tools.assert_in(resp.status_code, status_codes)
@with_setup(partial(setup_auth, 'user', '123'), teardown_auth)
@@ -40,15 +63,24 @@ class TestSmoke(BaseRequestTests):
def check_admin_status_code(self, *args, **kwargs):
self.check_status_code(*args, **kwargs)
- def test_anon(self):
+ def test_all_url_subs(self):
+ '''Testing that the testsuite is going to substitute all variables in the url.'''
+ for i in self.url_subs.values():
+ if i is None:
+ raise Exception('Smoke test must be updated for new url variable present in base.py')
+
+ def test_anon_view(self):
for url in anon_urls:
yield self.check_status_code, url, [200]
- def test_authed(self):
- for url in (u for u in authed_urls):
+ def test_authed_view(self):
+ for url in (u for u in authed_view_urls):
yield self.check_user_status_code, url, [200]
- def test_admin(self):
- self.setup_auth('admin', '123')
- for url in (u for u in admin_urls):
+ def test_admin_view(self):
+ for url in (u for u in admin_view_urls):
+ yield self.check_admin_status_code, url, [200]
+
+ def test_admin_modify(self):
+ for url in (u for u in admin_modify_urls):
yield self.check_admin_status_code, url, [200]
commit 4b3fcea5e4868054f8f8da80239a6e66e1183551
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 15:28:54 2012 -0700
Add ability to specify site-specifc css
diff --git a/development.cfg b/development.cfg
index 5993209..6e045f2 100644
--- a/development.cfg
+++ b/development.cfg
@@ -10,3 +10,5 @@ FAS_CHECK_CERT = False
FAS_HTTPS_REQUIRED = False
FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+# List of URLs for additional site-wide CSS
+EXTRA_CSS = []
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
index 75f2543..b167ba4 100644
--- a/fedora_elections/templates/base.html
+++ b/fedora_elections/templates/base.html
@@ -4,6 +4,8 @@
{% block head %}
<title>Fedora Elections{% if self.title() %} — {% block title %}{% endblock %}{% endif %}</title>
<link rel="stylesheet" href="{{ url_for("static", filename="style.css") }}">
+ {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
+ {% endfor %}
{% endblock %}
</head>
<body>
commit 36ded228f98e02af3da6ac14c49e2d2027734de7
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 15:18:49 2012 -0700
Fix traceback for hitting logout when the user is not logged in
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 9570055..d59fa64 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -80,7 +80,7 @@ def auth_login():
@app.route('/logout')
def auth_logout():
if not flask.g.fas_user:
- return safe_redirect_back()
+ return redirect.safe_redirect_back()
fas.logout()
flask.flash('You have been logged out')
return redirect.safe_redirect_back()
commit 7e5dec7001c0b6faa6721ecc235760cf25029158
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 15:18:14 2012 -0700
Add unittests -- currently just a few smoketests
diff --git a/testing b/testing
new file mode 100755
index 0000000..529e8ee
--- /dev/null
+++ b/testing
@@ -0,0 +1,2 @@
+#!/bin/sh
+FEDORA_ELECTIONS_CONFIG=../development.cfg nosetests $*
diff --git a/tests/base.py b/tests/base.py
new file mode 100644
index 0000000..8271e65
--- /dev/null
+++ b/tests/base.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2012 Red Hat, Inc.
+#
+# Author(s): Toshio Kuratomi <toshio(a)fedoraproject.org>
+
+import fedora_elections
+
+# Note: This is used in unittests that use nose's generator tests so we can't
+# subclass unittest.TestCase
+class BaseRequestTests(object):
+ '''Setup application for further testing of requests'''
+
+ def setUp(self):
+ fedora_elections.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + '../tests/testing.db'
+ fedora_elections.app.config['TESTING'] = True
+ self.app = fedora_elections.app.test_client()
+ fedora_elections.db.create_all()
+
+ def tearDown(self):
+ fedora_elections.db.session.remove()
+ fedora_elections.db.drop_all()
+
+anon_urls = ('/', '/archive', '/login', '/logout')
+admin_urls = ('/admin', '/admin/new', '/admin/%(election_alias)s',
+ '/admin/%(election_alias)s/edit',
+ '/admin/%(election_alias)s/candidates/new',
+ '/admin/%(election_alias)s/candidates/%(candidate_id)d/edit',
+ '/admin/%(election_alias)s/candidates/%(candidate_id)d/delete')
+authed_urls = ('/logout',)
diff --git a/tests/test_smoke.py b/tests/test_smoke.py
new file mode 100644
index 0000000..6b03e8e
--- /dev/null
+++ b/tests/test_smoke.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2012 Red Hat, Inc.
+#
+# Author(s): Toshio Kuratomi <toshio(a)fedoraproject.org>
+
+from functools import partial
+
+from nose import tools, with_setup
+
+from base import BaseRequestTests, anon_urls, authed_urls, admin_urls
+
+class TestSmoke(BaseRequestTests):
+ '''Basic test that we don't get 500s'''
+
+ def setUp(self):
+ super(TestSmoke, self).setUp()
+
+ def tearDown(self):
+ super(TestSmoke, self).tearDown()
+
+ def setup_auth(self, username, password):
+ self.setUp()
+ self.app.post('/login', data=dict( username=username,
+ password=password), follow_redirects=True)
+
+ def teardown_auth(self):
+ self.tearDown()
+ return self.app.get('/logout', follow_redirects=True)
+
+ def check_status_code(self, url, status_codes=[200]):
+ resp = self.app.get(url, follow_redirects=True)
+ tools.assert_in(resp.status_code, status_codes)
+
+ @with_setup(partial(setup_auth, 'user', '123'), teardown_auth)
+ def check_user_status_code(self, *args, **kwargs):
+ self.check_status_code(*args, **kwargs)
+
+ @with_setup(partial(setup_auth, 'admin', '123'), teardown_auth)
+ def check_admin_status_code(self, *args, **kwargs):
+ self.check_status_code(*args, **kwargs)
+
+ def test_anon(self):
+ for url in anon_urls:
+ yield self.check_status_code, url, [200]
+
+ def test_authed(self):
+ for url in (u for u in authed_urls):
+ yield self.check_user_status_code, url, [200]
+
+ def test_admin(self):
+ self.setup_auth('admin', '123')
+ for url in (u for u in admin_urls):
+ yield self.check_admin_status_code, url, [200]
commit e8098950006e1e3ae0d40638d412f0a182003703
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 11:27:11 2012 -0700
Add a TODO file
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..335107f
--- /dev/null
+++ b/TODO
@@ -0,0 +1,28 @@
+Things that need doing to get this deployed
+===========================================
+
+* Write a conversion script for the old db model to the new one
+* rpmify
+* rpmify flask-fas (or get flask-fas into python-fedora)
+* Implement a voting method for range voting
+
+* CSRF protection -- anything going through wtf forms is protected. It's up to
+ us to make sure that no data changing requests are coded without wtf forms.
+ So this is really: audit to make sure everything is using wtf forms
+
+Future enhancements (anything not necessary for deploying)
+==========================================================
+
+* Make voting methods modular
+* Give this a Fedora theme
+* Break the main program logic out into library modules
+** Controllers should be as lightweight as possible
+*** validation of input
+**** Might decorate the controller method but the validation itself should live
+ in another module. Might want to break validation schemas into another
+ module too
+*** Authentication
+**** decorators can go here and calls that return True or False. Logic itself
+ should go in a different module
+*** return data to templating engine
+*** Association with a URL
commit 8a345a12afebbbf1f5ed2e786d6155946e494a06
Author: Ralph Bean <rbean(a)redhat.com>
Date: Mon Aug 20 15:20:22 2012 -0400
Added preliminary requirements.txt.
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..f6f22bc
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,8 @@
+# Used for when working from a virtualenv.
+# Use this file by running "$ pip install -r requirements.txt"
+flask
+python-fedora
+kitchen
+Flask-FAS
+Flask-sqlalchemy
+Flask-wtf
commit 6d36e42e9d1b81d5c4e924c0282a303a8bd36ef7
Author: Ian Weller <ian(a)ianweller.org>
Date: Sun Jul 15 09:54:58 2012 -0500
List frontpaged elections on front page
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 00269bc..9570055 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -53,7 +53,7 @@ def election_admin_required(f):
@app.route('/')
def index():
elections = models.Election.query.filter_by(frontpage=True).all()
- return flask.render_template('list/index.html')
+ return flask.render_template('list/index.html', elections=elections)
@app.route('/archive')
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index b07108b..e40f882 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -1,6 +1,7 @@
{% extends "base.html" %}
+{% block title %}Election administration{% endblock %}
{% block body %}
-<h2>My elections</h2>
+<h2>Election administration</h2>
<table>
<tr>
<th>Election</th>
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 1ad207a..90aafa1 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -1,4 +1,26 @@
{% extends "base.html" %}
{% block body %}
-<p>Hello, world!</p>
+<h2>Open elections</h2>
+<table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{ election.summary }}</td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ <td>
+ {% if election.status == "In progress" %}
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">Vote now!</a>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+</table>
{% endblock %}
commit e21903e83b6f352324f05dcdaeaa01b3133ec8de
Author: Ian Weller <ian(a)ianweller.org>
Date: Thu Jul 12 16:59:14 2012 -0500
Initial commit with mostly-working admin
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bb585f9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+database.sqlite
+*.pyc
+*.pyo
+*.swp
diff --git a/createdb b/createdb
new file mode 100755
index 0000000..4699a56
--- /dev/null
+++ b/createdb
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_CONFIG=../development.cfg python createdb.py
diff --git a/createdb.py b/createdb.py
new file mode 100644
index 0000000..e99a35c
--- /dev/null
+++ b/createdb.py
@@ -0,0 +1,2 @@
+from fedora_elections import db
+db.create_all()
diff --git a/development.cfg b/development.cfg
new file mode 100644
index 0000000..5993209
--- /dev/null
+++ b/development.cfg
@@ -0,0 +1,12 @@
+import os, os.path
+
+DEBUG = True
+# You will want to change this for your install
+SECRET_KEY = 'change me'
+SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.getcwd(), 'database.sqlite')
+
+FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
+FAS_CHECK_CERT = False
+FAS_HTTPS_REQUIRED = False
+
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
new file mode 100644
index 0000000..00269bc
--- /dev/null
+++ b/fedora_elections/__init__.py
@@ -0,0 +1,207 @@
+import flask
+from flask.ext.fas import FAS
+from flask.ext.sqlalchemy import SQLAlchemy
+
+from datetime import datetime, time
+from functools import wraps
+from urlparse import urlparse, urljoin
+
+app = flask.Flask(__name__)
+app.config.from_object('fedora_elections.default_config')
+app.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
+
+# set up SQLAlchemy
+db = SQLAlchemy(app)
+
+# set up FAS
+fas = FAS(app)
+
+
+# modular imports
+from fedora_elections import forms, models, redirect
+
+
+def remove_csrf(form_data):
+ return dict([(k, v) for k, v in form_data.items() if k != 'csrf'])
+
+
+def login_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if flask.g.fas_user is None:
+ return flask.redirect(flask.url_for('auth_login',
+ next=flask.request.url))
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+def election_admin_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if flask.g.fas_user is None:
+ return flask.redirect(flask.url_for('auth_login',
+ next=flask.request.url))
+ if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
+ flask.g.fas_user.groups:
+ flask.abort(403)
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+### LIST VIEWS #############################################
+
+(a)app.route('/')
+def index():
+ elections = models.Election.query.filter_by(frontpage=True).all()
+ return flask.render_template('list/index.html')
+
+
+(a)app.route('/archive')
+def archived_elections():
+ raise NotImplementedError
+
+
+### AUTH VIEWS #############################################
+
+(a)app.route('/login', methods=('GET', 'POST'))
+def auth_login():
+ if flask.g.fas_user:
+ return redirect.safe_redirect_back()
+ form = forms.LoginForm()
+ if form.validate_on_submit():
+ if fas.login(form.username.data, form.password.data):
+ flask.flash('Welcome, %s' % flask.g.fas_user.username)
+ return redirect.safe_redirect_back()
+ else:
+ flask.flash('Incorrect username or password')
+ return flask.render_template('auth/login.html', form=form)
+
+
+(a)app.route('/logout')
+def auth_logout():
+ if not flask.g.fas_user:
+ return safe_redirect_back()
+ fas.logout()
+ flask.flash('You have been logged out')
+ return redirect.safe_redirect_back()
+
+
+### ELECTION ADMIN VIEWS ###################################
+
+(a)app.route('/admin/')
+@election_admin_required
+def admin_view_elections():
+ elections = models.Election.query.filter_by().all()
+ return flask.render_template('admin/all_elections.html',
+ elections=elections)
+
+
+(a)app.route('/admin/new', methods=("GET", "POST"))
+@election_admin_required
+def admin_new_election():
+ form = forms.ElectionForm()
+ if form.validate_on_submit():
+ election = models.Election(**remove_csrf(form.data))
+ # Fix start_date and end_date to use datetime
+ election.start_date = datetime.combine(election.start_date, time())
+ election.end_date = datetime.combine(election.end_date,
+ time(23, 59, 59))
+ db.session.add(election)
+ admin = models.ElectionAdminGroup(election=election,
+ group_name=app.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ role_required=u'user')
+ db.session.add(admin)
+ db.session.commit()
+ flask.flash('Election "%s" added' % election.summary)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/election_form.html', form=form,
+ submit_text='Create election')
+
+
+(a)app.route('/admin/<election_alias>/')
+@election_admin_required
+def admin_view_election(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ return flask.render_template('admin/view_election.html', election=election)
+
+
+(a)app.route('/admin/<election_alias>/edit', methods=("GET", "POST"))
+@election_admin_required
+def admin_edit_election(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ form = forms.ElectionForm(election.id, obj=election)
+ if form.validate_on_submit():
+ form.populate_obj(election)
+ db.session.commit()
+ flask.flash('Election "%s" saved' % election.summary)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/election_form.html', form=form,
+ submit_text='Edit election')
+
+
+(a)app.route('/admin/<election_alias>/candidates/new', methods=("GET", "POST"))
+@election_admin_required
+def admin_add_candidate(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ form = forms.CandidateForm()
+ if form.validate_on_submit():
+ candidate = models.Candidate(election=election,
+ **remove_csrf(form.data))
+ db.session.add(candidate)
+ db.session.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/candidate.html', form=form,
+ submit_text='Add candidate')
+
+
+(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
+ methods=("GET", "POST"))
+@election_admin_required
+def admin_edit_candidate(election_alias, candidate_id):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ if not election or not candidate:
+ flask.abort(404)
+ if candidate.election != election:
+ flask.abort(404)
+ form = forms.CandidateForm(obj=candidate)
+ if form.validate_on_submit():
+ form.populate_obj(candidate)
+ db.session.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/candidate.html', form=form,
+ submit_text='Edit candidate')
+
+
+(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
+ methods=("GET", "POST"))
+@election_admin_required
+def admin_delete_candidate(election_alias, candidate_id):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ if not election or not candidate:
+ flask.abort(404)
+ if candidate.election != election:
+ flask.abort(404)
+ form = forms.ConfirmationForm()
+ if form.validate_on_submit():
+ db.session.delete(candidate)
+ candidate_name = candidate.name
+ db.session.commit()
+ flask.flash('Candidate "%s" deleted' % candidate_name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/delete_candidate.html', form=form,
+ candidate=candidate)
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
new file mode 100644
index 0000000..7b5b794
--- /dev/null
+++ b/fedora_elections/default_config.py
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
new file mode 100644
index 0000000..f050dbd
--- /dev/null
+++ b/fedora_elections/forms.py
@@ -0,0 +1,69 @@
+from flask.ext import wtf
+
+from fedora_elections.models import Election
+
+
+class ElectionForm(wtf.Form):
+ summary = wtf.TextField('Summary', [wtf.validators.Required(),
+ wtf.validators.Length(max=150)])
+ alias = wtf.TextField('Alias', [
+ wtf.validators.Required(),
+ wtf.validators.Length(max=100),
+ wtf.validators.Regexp('[a-z0-9_-]+',
+ message=('Alias may only contain lower case '
+ 'letters, numbers, hyphens and '
+ 'underscores.')),
+ ])
+ description = wtf.TextAreaField('Description', [wtf.validators.Required()])
+ url = wtf.TextField('URL', [wtf.validators.Required(),
+ wtf.validators.URL(),
+ wtf.validators.Length(max=250)])
+ start_date = wtf.DateField('Start date', [wtf.validators.Required()])
+ end_date = wtf.DateField('End date', [wtf.validators.Required()])
+ number_elected = wtf.IntegerField('Number elected',
+ [wtf.validators.Required(),
+ wtf.validators.NumberRange(min=1)],
+ default=1)
+ candidates_are_fasusers = wtf.BooleanField('Candidates are FAS users?')
+ frontpage = wtf.BooleanField('Show on front page?')
+ embargoed = wtf.BooleanField('Embargo results?', default=True)
+
+ def __init__(form, election_id=None, *args, **kwargs):
+ super(ElectionForm, form).__init__(*args, **kwargs)
+ form._election_id = election_id
+
+ def validate_summary(form, field):
+ check = Election.query.filter_by(summary=form.summary.data).all()
+ if check:
+ if not (form._election_id and form._election_id == check[0].id):
+ raise wtf.ValidationError('There is already another election '
+ 'with this summary.')
+
+ def validate_alias(form, field):
+ if form.alias.data == 'new':
+ raise wtf.ValidationError(flask.Markup('The alias cannot be '
+ '<code>new</code>.'))
+ check = Election.query.filter_by(alias=form.alias.data).all()
+ if check:
+ if not (form._election_id and form._election_id == check[0].id):
+ raise wtf.ValidationError('There is already another election '
+ 'with this alias.')
+
+ def validate_end_date(form, field):
+ if form.end_date.data <= form.start_date.data:
+ raise wtf.ValidationError('End date must be later than '
+ 'start date.')
+
+
+class CandidateForm(wtf.Form):
+ name = wtf.TextField('Name', [wtf.validators.Required()])
+ url = wtf.TextField('URL', [wtf.validators.Length(max=250)])
+
+
+class LoginForm(wtf.Form):
+ username = wtf.TextField('Username', [wtf.validators.Required()])
+ password = wtf.PasswordField('Password', [wtf.validators.Required()])
+
+
+class ConfirmationForm(wtf.Form):
+ pass
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
new file mode 100644
index 0000000..f025255
--- /dev/null
+++ b/fedora_elections/models.py
@@ -0,0 +1,94 @@
+from fedora_elections import app, db
+
+from datetime import datetime
+
+
+class Election(db.Model):
+ __tablename__ = 'elections'
+
+ id = db.Column(db.Integer, primary_key=True)
+ summary = db.Column(db.Unicode(150), unique=True, nullable=False)
+ alias = db.Column(db.Unicode(100), unique=True, nullable=False)
+ description = db.Column(db.UnicodeText, nullable=False)
+ url = db.Column(db.Unicode(250), nullable=False)
+ start_date = db.Column(db.DateTime, nullable=False)
+ end_date = db.Column(db.DateTime, nullable=False)
+ number_elected = db.Column(db.Integer, nullable=False, default=1)
+ embargoed = db.Column(db.Boolean, nullable=False, default=True)
+ frontpage = db.Column(db.Boolean, nullable=False, default=False)
+ voting_type = db.Column(db.Unicode(100), nullable=False, default=u'range')
+ candidates_are_fasusers = db.Column(db.Boolean, nullable=False,
+ default=False)
+
+ @property
+ def status(self):
+ if datetime.now() < self.start_date:
+ return 'Pending'
+ else:
+ if datetime.now() < self.end_date:
+ return 'In progress'
+ else:
+ if self.embargoed:
+ return 'Embargoed'
+ else:
+ return 'Ended'
+
+ @property
+ def locked(self):
+ return datetime.now() >= self.start_date
+
+
+class ElectionAdminGroup(db.Model):
+ __tablename__ = 'electionadmingroups'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('admin_groups',
+ lazy='dynamic'))
+ group_name = db.Column(db.Unicode(150), nullable=False)
+ role_required = db.Column(db.Enum(u'user', u'sponsor', u'administrator'),
+ nullable=False)
+
+
+class Candidate(db.Model):
+ __tablename__ = 'candidates'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('candidates',
+ lazy='dynamic'))
+ name = db.Column(db.Unicode(150), nullable=False) # FAS username if
+ # candidates_are_fasusers
+ url = db.Column(db.Unicode(250))
+
+
+class LegalVoter(db.Model):
+ __tablename__ = 'legalvoters'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('legal_voters',
+ lazy='dynamic'))
+ # special names:
+ # "cla + one" = cla_done + 1 non-cla group
+ group_name = db.Column(db.Unicode(150), nullable=False)
+
+
+class Vote(db.Model):
+ __tablename__ = 'votes'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('votes',
+ lazy='dynamic'))
+ voter = db.Column(db.Unicode(150), nullable=False)
+ timestamp = db.Column(db.DateTime, nullable=False)
+ candidate_id = db.Column(db.Integer, db.ForeignKey('candidates.id'),
+ nullable=False)
+ candidate = db.relationship('Candidate', backref=db.backref('votes',
+ lazy='dynamic'))
+ value = db.Column(db.Integer, nullable=False)
diff --git a/fedora_elections/redirect.py b/fedora_elections/redirect.py
new file mode 100644
index 0000000..5571abe
--- /dev/null
+++ b/fedora_elections/redirect.py
@@ -0,0 +1,23 @@
+import flask
+
+from urlparse import urljoin, urlparse
+
+
+def is_safe_url(target):
+ ref_url = urlparse(flask.request.host_url)
+ test_url = urlparse(urljoin(flask.request.host_url, target))
+ return test_url.scheme in ('http', 'https') and \
+ ref_url.netloc == test_url.netloc
+
+
+def safe_redirect_back(next=None, fallback=('index', {})):
+ targets = []
+ if next:
+ targets.append(next)
+ if 'next' in flask.request.args and \
+ flask.request.args['next']:
+ targets.append(flask.request.args['next'])
+ targets.append(flask.url_for(fallback[0], **fallback[1]))
+ for target in targets:
+ if is_safe_url(target):
+ return flask.redirect(target)
diff --git a/fedora_elections/static/style.css b/fedora_elections/static/style.css
new file mode 100644
index 0000000..b316ea7
--- /dev/null
+++ b/fedora_elections/static/style.css
@@ -0,0 +1,90 @@
+body {
+ font-family: sans-serif;
+ font-size: 12pt;
+ line-height: 1.35;
+}
+
+#flashes {
+ background-color: #ffa;
+}
+
+nav {
+ background-color: #eee;
+ padding: 0.5em 1em;
+}
+
+nav h2 {
+ display: inline;
+ font-size: inherit;
+ margin-right: 1em;
+}
+
+nav ul {
+ display: inline;
+ list-style: none;
+ padding: 0;
+}
+
+nav li {
+ display: inline;
+ margin-right: .5em;
+}
+
+.clear {
+ clear: both;
+}
+
+/* input elements */
+
+fieldset {
+ border: 0;
+ padding: 0;
+ margin: .5em 0;
+ float: left;
+ clear: both;
+}
+
+.row {
+ margin: .25em 0;
+}
+
+.row label:first-child {
+ width: 8em;
+ margin-right: 0.75em;
+ text-align: right;
+ clear: both;
+ font-weight: bold;
+ display: inline-block;
+ vertical-align: top;
+}
+
+.row textarea {
+ width: 30em;
+ height: 8em;
+}
+
+form .row .errors {
+ list-style: none;
+ margin: .5em 0 1em 10.9375em;
+ padding: 0;
+ font-size: .8em;
+ color: #c00;
+ width: 30em;
+}
+
+div.help {
+ width: 40%;
+ float: right;
+ background-color: #ffa;
+ border-top: 1px solid #ee9;
+ padding: 1em;
+ font-size: .8em;
+ margin: .625em 0;
+}
+
+div > *:first-child { margin-top: 0; }
+div > *:last-child { margin-bottom: 0; }
+
+form .buttons.indent {
+ margin-left: 8.75em;
+}
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
new file mode 100644
index 0000000..ff781b7
--- /dev/null
+++ b/fedora_elections/templates/_formhelpers.html
@@ -0,0 +1,13 @@
+{% macro render_field(field, after="") %}
+<div class="row">
+ {{ field.label }}{{ field(**kwargs)|safe }}{% if after %} {{ after }}{% endif %}
+ {% if field.errors %}
+ <ul class="errors">
+ {% for error in field.errors %}
+ <li>{{ error }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+</div>
+{% endmacro %}
+{# vim: set ts=2 sw=2 sta et: #}
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
new file mode 100644
index 0000000..b07108b
--- /dev/null
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -0,0 +1,20 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>My elections</h2>
+<table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td><a href="{{ url_for('admin_view_election', election_alias=election.alias) }}">{{ election.summary }}</a></td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ </tr>
+ {% endfor %}
+</table>
+{% endblock %}
diff --git a/fedora_elections/templates/admin/candidate.html b/fedora_elections/templates/admin/candidate.html
new file mode 100644
index 0000000..9546da0
--- /dev/null
+++ b/fedora_elections/templates/admin/candidate.html
@@ -0,0 +1,17 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}{{ submit_text }}{% endblock %}
+{% block body %}
+<h2>{{ submit_text }}</h2>
+
+<form action="" method="post">
+ <fieldset>
+ {{ render_field(form.name) }}
+ {{ render_field(form.url) }}
+ </fieldset>
+
+ <br class="clear">
+ <p class="buttons"><input type="submit" class="submit positive" value="{{ submit_text }}"></p>
+ {{ form.csrf_token }}
+</form>
+{% endblock %}
diff --git a/fedora_elections/templates/admin/delete_candidate.html b/fedora_elections/templates/admin/delete_candidate.html
new file mode 100644
index 0000000..3cfe4be
--- /dev/null
+++ b/fedora_elections/templates/admin/delete_candidate.html
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}Delete candidate{% endblock %}
+{% block body %}
+<h2>{{ self.title() }}</h2>
+
+<form action="" method="post">
+ <p>Are you sure you want to delete candidate "{{ candidate.name }}"?</p>
+ <p class="buttons"><input type="submit" value="Delete candidate">
+ <a href="{{ url_for('admin_view_election', election_alias=candidate.election.alias) }}">Cancel</a></p>
+ {{ form.csrf_token }}
+</form>
+{% endblock %}
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
new file mode 100644
index 0000000..e9d05c7
--- /dev/null
+++ b/fedora_elections/templates/admin/election_form.html
@@ -0,0 +1,62 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}{{ submit_text }}{% endblock %}
+{% block body %}
+<h2>{{ self.title() }}</h2>
+
+<form action="" method="post">
+ <fieldset>
+ {{ render_field(form.summary) }}
+ </fieldset>
+ <div class="help">
+ <p>This is the name of the election that users will first see.</p>
+ <p>Example: <kbd>Fedora Board, November 2010</kbd></p>
+ </div>
+ <fieldset>
+ {{ render_field(form.alias) }}
+ </fieldset>
+ <div class="help">
+ <p>A short, unique name that will be used for the election's URL. Lowercase letters, numbers, hyphens and underscores only.</p>
+ <p>Example: <kbd>boardf15</kbd></p>
+ </div>
+ <fieldset>
+ {{ render_field(form.description) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.url) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.start_date, " at 00:00:00 UTC") }}
+ {{ render_field(form.end_date, " at 23:59:59 UTC") }}
+ </fieldset>
+ <div class="help">
+ <p>Enter dates in <kbd>YYYY-MM-DD</kbd> format.</p>
+ </div>
+ <fieldset>
+ {{ render_field(form.number_elected) }}
+ </fieldset>
+ <div class="help">
+ <p>This determines the cutoff for how many candidates win the election. For example, if you are electing four empty seats on a committee, enter <kbd>4</kbd> here.</p>
+ </div>
+ <fieldset>
+ {{ render_field(form.candidates_are_fasusers) }}
+ </fieldset>
+ <div class="help">
+ <p>Check this if every candidate in this election has a FAS user account. This will automatically pull full names and wiki page URLs from FAS.</p>
+ </div>
+ <fieldset>
+ {{ render_field(form.frontpage) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.embargoed) }}
+ </fieldset>
+ <div class="help">
+ <p>Embargo results when the election is finished. To unembargo them, edit the election and uncheck this box.</p>
+ </div>
+
+ <br class="clear">
+ {% block election_form_extra %}{% endblock %}
+ <p class="buttons"><input type="submit" class="submit positive" value="{{ submit_text }}"></p>
+ {{ form.csrf_token }}
+</form>
+{% endblock %}
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
new file mode 100644
index 0000000..4b5b174
--- /dev/null
+++ b/fedora_elections/templates/admin/view_election.html
@@ -0,0 +1,26 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{ election.summary }}</h2>
+<p>{{ election.description }}</p>
+<p><a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">Edit election</a>
+<ul>
+ <li>On front page: {{ election.frontpage }}</li>
+ <li>Start date: {{ election.start_date }}</li>
+ <li>End date: {{ election.end_date }}</li>
+ <li>Number elected: {{ election.number_elected }}</li>
+ <li>Election type: {{ election.voting_type }}</li>
+</ul>
+<h3>Candidates</h3>
+{% if election.candidates.all() %}
+<ul>
+ {% for candidate in election.candidates.all() %}
+ <li>{{ candidate.name }}
+ <a href="{{ url_for('admin_edit_candidate', election_alias=election.alias, candidate_id=candidate.id) }}">[edit]</a>
+ <a href="{{ url_for('admin_delete_candidate', election_alias=election.alias, candidate_id=candidate.id) }}">[x]</a></li>
+ {% endfor %}
+</ul>
+{% else %}
+<p>There are no candidates.</p>
+{% endif %}
+<a href="{{ url_for('admin_add_candidate', election_alias=election.alias) }}">Add a candidate</a>
+{% endblock %}
diff --git a/fedora_elections/templates/auth/login.html b/fedora_elections/templates/auth/login.html
new file mode 100644
index 0000000..aafe3a8
--- /dev/null
+++ b/fedora_elections/templates/auth/login.html
@@ -0,0 +1,14 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}Log in{% endblock %}
+{% block body %}
+<h2>Log in</h2>
+
+<form action="" method="post">
+ <p>Log in using your <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a> account.</p>
+ {{ render_field(form.username) }}
+ {{ render_field(form.password) }}
+ <p class="buttons indent"><input type="submit" class="submit positive" value="Log in"></p>
+ {{ form.csrf_token }}
+</form>
+{% endblock %}
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
new file mode 100644
index 0000000..75f2543
--- /dev/null
+++ b/fedora_elections/templates/base.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ {% block head %}
+ <title>Fedora Elections{% if self.title() %} — {% block title %}{% endblock %}{% endif %}</title>
+ <link rel="stylesheet" href="{{ url_for("static", filename="style.css") }}">
+ {% endblock %}
+ </head>
+ <body>
+ <h1>Fedora Elections</h1>
+ {% if g.fas_user %}
+ <p>Hello, {{ g.fas_user.username }} — <a href="{{ url_for("auth_logout") }}">log out</a></p>
+ {% else %}
+ <p>You are not logged in — <a href="{{ url_for("auth_login") }}">log in</a></p>
+ {% endif %}
+ <nav>
+ <h2>Elections</h2>
+ <ul>
+ <li><a href="{{ url_for("index") }}">Open elections</a></li>
+ <li><a href="{{ url_for("archived_elections") }}">Archived elections</a></li>
+ </ul>
+ <br>
+ <h2>Administration</h2>
+ <ul>
+ <li><a href="{{ url_for("admin_view_elections") }}">My elections</a></li>
+ <li><a href="{{ url_for("admin_new_election") }}">Create new election</a></li>
+ </ul>
+ </nav>
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+ <ul id="flashes">
+ {% for message in messages %}
+ <li>{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {% endwith %}
+ {% block body %}{% endblock %}
+ </body>
+</html>
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
new file mode 100644
index 0000000..1ad207a
--- /dev/null
+++ b/fedora_elections/templates/list/index.html
@@ -0,0 +1,4 @@
+{% extends "base.html" %}
+{% block body %}
+<p>Hello, world!</p>
+{% endblock %}
diff --git a/runserver b/runserver
new file mode 100755
index 0000000..39898a2
--- /dev/null
+++ b/runserver
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_CONFIG=../development.cfg python runserver.py
diff --git a/runserver.py b/runserver.py
new file mode 100644
index 0000000..176e2b9
--- /dev/null
+++ b/runserver.py
@@ -0,0 +1,2 @@
+from fedora_elections import app
+app.run()
diff --git a/shell b/shell
new file mode 100755
index 0000000..fd475b5
--- /dev/null
+++ b/shell
@@ -0,0 +1,7 @@
+#!/bin/bash
+which ipython > /dev/null 2>&1
+if [ "$?" == "0" ]; then
+ FEDORA_ELECTIONS_CONFIG=../development.cfg ipython
+else
+ FEDORA_ELECTIONS_CONFIG=../development.cfg python
+fi
9 years, 10 months
[elections-guide] Updated Rev_hx
by Eric Christensen
commit effcb42a4ff67e0dd9392bb5526827fb5426fb3d
Author: Eric Christensen <echriste(a)redhat.com>
Date: Mon Jul 14 11:41:54 2014 -0400
Updated Rev_hx
en-US/Revision_History.xml | 14 ++++++++++++++
1 files changed, 14 insertions(+), 0 deletions(-)
---
diff --git a/en-US/Revision_History.xml b/en-US/Revision_History.xml
index 37e4226..4b0c3a4 100644
--- a/en-US/Revision_History.xml
+++ b/en-US/Revision_History.xml
@@ -4,6 +4,20 @@
<revhistory>
<revision>
+ <revnumber>1.1</revnumber>
+ <date>Monday July 14 2014</date>
+ <author>
+ <firstname>Eric</firstname>
+ <surname>Christensen</surname>
+ <email>sparks(a)fedoraproject.org</email>
+ </author>
+ <revdescription>
+ <simplelist>
+ <member>Fixed spelling errors.</member>
+ </simplelist>
+ </revdescription>
+ </revision>
+ <revision>
<revnumber>1.0</revnumber>
<date>Monday November 22 2010</date>
<author>
9 years, 10 months
[elections-guide] Fixed a spelling error
by Eric Christensen
commit 66c1723900fc92ed91055ef0359b1590fb4f022d
Author: Eric Christensen <echriste(a)redhat.com>
Date: Mon Jul 14 11:39:24 2014 -0400
Fixed a spelling error
en-US/Methods.xml | 2 +-
1 files changed, 1 insertions(+), 1 deletions(-)
---
diff --git a/en-US/Methods.xml b/en-US/Methods.xml
index 15b3ce9..71539f1 100644
--- a/en-US/Methods.xml
+++ b/en-US/Methods.xml
@@ -26,7 +26,7 @@
<section id="sect-Fedora_Elections_Guide-Range_Voting-Range_Voting_Example">
<title>Range Voting Example</title>
<para>
- To accurately demostrate the concepts of Range Voting in action, it is necessary to use a fictional election where 7 candidates are standing (Candidates A to G). If three eligible voters were to choose their level of preference as explained in <xref linkend="sect-Fedora_Elections_Guide-Range_Voting-Concepts_of_Range_Voting" /> a data table such as <xref linkend="tabl-Fedora_Elections_Guide-Range_Voting_Example-Simple_Example_Ballot" /> can be formed to display the data and demonstrate how a winner would be defined.
+ To accurately demonstrate the concepts of Range Voting in action, it is necessary to use a fictional election where 7 candidates are standing (Candidates A to G). If three eligible voters were to choose their level of preference as explained in <xref linkend="sect-Fedora_Elections_Guide-Range_Voting-Concepts_of_Range_Voting" /> a data table such as <xref linkend="tabl-Fedora_Elections_Guide-Range_Voting_Example-Simple_Example_Ballot" /> can be formed to display the data and demonstrate how a winner would be defined.
</para>
<table frame="all" id="tabl-Fedora_Elections_Guide-Range_Voting_Example-Simple_Example_Ballot">
<title>Simple Example Ballot</title>
9 years, 10 months
[elections-guide] Fix a couple of typos
by Eric Christensen
commit 1b1e295332c177bfe1636811f2bdf55f0200386a
Author: David King <amigadave(a)amigadave.com>
Date: Sat Jul 12 09:31:43 2014 +0100
Fix a couple of typos
https://bugzilla.redhat.com/show_bug.cgi?id=1118954
Signed-off-by: Eric Christensen <echriste(a)redhat.com>
en-US/Methods.xml | 4 ++--
1 files changed, 2 insertions(+), 2 deletions(-)
---
diff --git a/en-US/Methods.xml b/en-US/Methods.xml
index 65ffe58..15b3ce9 100644
--- a/en-US/Methods.xml
+++ b/en-US/Methods.xml
@@ -26,7 +26,7 @@
<section id="sect-Fedora_Elections_Guide-Range_Voting-Range_Voting_Example">
<title>Range Voting Example</title>
<para>
- To accurately demostrate the concepts of Range Voting in action, it is necessary to use a fictional election where 7 candidates are standing (Candidates A to G). If three eligible voters were to choose their level of perference as explained in <xref linkend="sect-Fedora_Elections_Guide-Range_Voting-Concepts_of_Range_Voting" /> a data table such as <xref linkend="tabl-Fedora_Elections_Guide-Range_Voting_Example-Simple_Example_Ballot" /> can be formed to display the data and demonstrate how a winner would be defined.
+ To accurately demostrate the concepts of Range Voting in action, it is necessary to use a fictional election where 7 candidates are standing (Candidates A to G). If three eligible voters were to choose their level of preference as explained in <xref linkend="sect-Fedora_Elections_Guide-Range_Voting-Concepts_of_Range_Voting" /> a data table such as <xref linkend="tabl-Fedora_Elections_Guide-Range_Voting_Example-Simple_Example_Ballot" /> can be formed to display the data and demonstrate how a winner would be defined.
</para>
<table frame="all" id="tabl-Fedora_Elections_Guide-Range_Voting_Example-Simple_Example_Ballot">
<title>Simple Example Ballot</title>
@@ -172,7 +172,7 @@
In this example, Candidate C would be declared the winner with a total vote of 15 as opposed to Candidate A's 14 votes.
</para>
<para>
- Zero votes have not altered the result due to the summing method been used and can be intrepreted as the voter having no preference or abstaining.
+ Zero votes have not altered the result due to the summing method being used and can be intrepreted as the voter having no preference or abstaining.
</para>
</section>
9 years, 10 months
Branch 'develop' - 2 commits - README README.rst
by Pierre-YvesChibon
README | 100 ----------------------------------------------
README.rst | 132 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 132 insertions(+), 100 deletions(-)
New commits:
commit 0c0a63e658bac098dbf99814e05cdaecf2454d5e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat Jul 12 00:07:21 2014 +0200
Make the README file an rst file
diff --git a/README b/README
deleted file mode 100644
index 42dc6de..0000000
--- a/README
+++ /dev/null
@@ -1,132 +0,0 @@
-================
-Fedora Elections
-================
-
-* What is fedora-election?
-
-fedora-elections is a web application written in python and based on flask.
-It implements the Range Voting system (http://rangevoting.org).
-
-This project was developed using fedoraproject requests, but, can be easily
-adapted to other projects. Fedora-elections is integrated with the Fedora
-Account System (FAS).
-
-
-Running from a checkout:
-========================
-
-Install Prerequisites
----------------------
-Before launching fedora-elections, the following packages should be installed:
- - httpd
- - libxslt
- - python
- - python-backports-ssl_match_hostname
- - python-bunch
- - python-chardet
- - python-fedora
- - python-fedora-flask
- - python-flask
- - python-flask-sqlalchemy
- - python-flask-wtf
- - python-jinja2
- - python-kitchen
- - python-lxml
- - python-openid
- - python-ordereddict
- - python-ordereddict
- - python-setuptools
- - python-simplejson
- - python-six
- - python-sqlalchemy0.7
- - python-urllib3
- - python-wtforms
-
-
-Get the source code
--------------------
-The project is hosted on https://fedorahosted.org/
-
-More precisely at: https://fedorahosted.org/elections
-
-You can obtain the code via:
-
- ::
-
- git clone http://git.fedorahosted.org/git/elections.git
-
-For commodity reason, a clone is available on github:
-https://github.com/fedora-infra/elections
-
-
-Configure the application
--------------------------
-An example configuration file is provided at: ``files/fedora-elections.cfg``
-
-
-Create a database
------------------
-Run:
-
- ::
-
- python createdb.py
-
-
-Starting the Application
-------------------------
-
-There are 2 ways to start the application:
- * without apache
- * with apache
-
-
-* How to start without apache on localhost:5000 (useful for development):
-
- ::
-
- ./runserver
-
-
-* How to start with http
-
- Next copy the file ``fedora-elections.conf`` file to your apache conf.d
- directory:
-
- ::
-
- sudo cp files/fedora-elections.conf /etc/httpd/conf.d/.
-
- Place the file ``fedora-elections.wsgi`` for example in /var/www
-
- ::
-
- sudo cp files/fedora-elections.wsgi /var/www
-
- Adjust the apache configuration file to point to it
-
- Adjust the wsgi file installed in /var/www to point to fedora_elections
-
-
- Place the fedora-elections configuration file in
- ``/etc/fedora-elections/fedora-elections.cfg``
-
- ::
-
- sudo mkdir -p /etc/fedora-elections/
- sudo cp files/fedora-elections.cfg /etc/fedora-elections/
-
- Restart apache:
-
- sudo /etc/init.d/httpd restart
-
-* How to contribute
-
-If you find bug or want to propose ideas or stuff to be implemented or if
-you are interested to became a developer for this project just
-ask on #fedora-admin irc channel on irc.freenode.net or use our
-web site https://fedorahosted.org/elections.
-
-* Licence
-
-fedora-elections is licenced under GPL v2.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..42dc6de
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,132 @@
+================
+Fedora Elections
+================
+
+* What is fedora-election?
+
+fedora-elections is a web application written in python and based on flask.
+It implements the Range Voting system (http://rangevoting.org).
+
+This project was developed using fedoraproject requests, but, can be easily
+adapted to other projects. Fedora-elections is integrated with the Fedora
+Account System (FAS).
+
+
+Running from a checkout:
+========================
+
+Install Prerequisites
+---------------------
+Before launching fedora-elections, the following packages should be installed:
+ - httpd
+ - libxslt
+ - python
+ - python-backports-ssl_match_hostname
+ - python-bunch
+ - python-chardet
+ - python-fedora
+ - python-fedora-flask
+ - python-flask
+ - python-flask-sqlalchemy
+ - python-flask-wtf
+ - python-jinja2
+ - python-kitchen
+ - python-lxml
+ - python-openid
+ - python-ordereddict
+ - python-ordereddict
+ - python-setuptools
+ - python-simplejson
+ - python-six
+ - python-sqlalchemy0.7
+ - python-urllib3
+ - python-wtforms
+
+
+Get the source code
+-------------------
+The project is hosted on https://fedorahosted.org/
+
+More precisely at: https://fedorahosted.org/elections
+
+You can obtain the code via:
+
+ ::
+
+ git clone http://git.fedorahosted.org/git/elections.git
+
+For commodity reason, a clone is available on github:
+https://github.com/fedora-infra/elections
+
+
+Configure the application
+-------------------------
+An example configuration file is provided at: ``files/fedora-elections.cfg``
+
+
+Create a database
+-----------------
+Run:
+
+ ::
+
+ python createdb.py
+
+
+Starting the Application
+------------------------
+
+There are 2 ways to start the application:
+ * without apache
+ * with apache
+
+
+* How to start without apache on localhost:5000 (useful for development):
+
+ ::
+
+ ./runserver
+
+
+* How to start with http
+
+ Next copy the file ``fedora-elections.conf`` file to your apache conf.d
+ directory:
+
+ ::
+
+ sudo cp files/fedora-elections.conf /etc/httpd/conf.d/.
+
+ Place the file ``fedora-elections.wsgi`` for example in /var/www
+
+ ::
+
+ sudo cp files/fedora-elections.wsgi /var/www
+
+ Adjust the apache configuration file to point to it
+
+ Adjust the wsgi file installed in /var/www to point to fedora_elections
+
+
+ Place the fedora-elections configuration file in
+ ``/etc/fedora-elections/fedora-elections.cfg``
+
+ ::
+
+ sudo mkdir -p /etc/fedora-elections/
+ sudo cp files/fedora-elections.cfg /etc/fedora-elections/
+
+ Restart apache:
+
+ sudo /etc/init.d/httpd restart
+
+* How to contribute
+
+If you find bug or want to propose ideas or stuff to be implemented or if
+you are interested to became a developer for this project just
+ask on #fedora-admin irc channel on irc.freenode.net or use our
+web site https://fedorahosted.org/elections.
+
+* Licence
+
+fedora-elections is licenced under GPL v2.
commit ec39d749fe914a60448baf86d14b8962a78ded14
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat Jul 12 00:06:02 2014 +0200
Update the documentation in the README file
diff --git a/README b/README
index 4ab94e0..42dc6de 100644
--- a/README
+++ b/README
@@ -1,8 +1,9 @@
================
Fedora Elections
-===============
+================
* What is fedora-election?
+
fedora-elections is a web application written in python and based on flask.
It implements the Range Voting system (http://rangevoting.org).
@@ -12,7 +13,7 @@ Account System (FAS).
Running from a checkout:
-=======================
+========================
Install Prerequisites
---------------------
@@ -44,57 +45,88 @@ Before launching fedora-elections, the following packages should be installed:
Get the source code
-------------------
-You can obtain the code from github:
- https://github.com/fedora-infra/elections.git
+The project is hosted on https://fedorahosted.org/
+
+More precisely at: https://fedorahosted.org/elections
+
+You can obtain the code via:
+
+ ::
+
+ git clone http://git.fedorahosted.org/git/elections.git
-git clone https://github.com/fedora-infra/elections.git
+For commodity reason, a clone is available on github:
+https://github.com/fedora-infra/elections
Configure the application
-------------------------
-cd fedora_elections
-../configure.py --install-conf=${PWD%/*} --install-data=${PWD%/*} --shared-state=${PWD%/*}
+An example configuration file is provided at: ``files/fedora-elections.cfg``
-This will create createdb, fedora-elections.cfg, fedora-elections.wsgi and
-http-fedora-elections.conf.
-
-Modify fedora-elections.cfg to change FAS_PASSWORD to the correct password.
Create a database
-----------------
-Copy development.cfg.sample to development.cfg.
-Review the file and make changes as appropriate.
Run:
- ./createdb
+
+ ::
+
+ python createdb.py
Starting the Application
------------------------
There are 2 ways to start the application:
- o without http
- o with http
+ * without apache
+ * with apache
-* How to start without http on localhost:5000:
- cd elections
+* How to start without apache on localhost:5000 (useful for development):
+
+ ::
+
./runserver
* How to start with http
- Next copy the httpd-pkgdb.conf file to your apache conf.d directory:
- sudo cp httpd-fedora-elections.conf /etc/httpd/conf.d/.
+ Next copy the file ``fedora-elections.conf`` file to your apache conf.d
+ directory:
+
+ ::
+
+ sudo cp files/fedora-elections.conf /etc/httpd/conf.d/.
+
+ Place the file ``fedora-elections.wsgi`` for example in /var/www
+
+ ::
+
+ sudo cp files/fedora-elections.wsgi /var/www
+
+ Adjust the apache configuration file to point to it
+
+ Adjust the wsgi file installed in /var/www to point to fedora_elections
+
+
+ Place the fedora-elections configuration file in
+ ``/etc/fedora-elections/fedora-elections.cfg``
+
+ ::
+
+ sudo mkdir -p /etc/fedora-elections/
+ sudo cp files/fedora-elections.cfg /etc/fedora-elections/
Restart apache:
sudo /etc/init.d/httpd restart
* How to contribute
+
If you find bug or want to propose ideas or stuff to be implemented or if
you are interested to became a developer for this project just
ask on #fedora-admin irc channel on irc.freenode.net or use our
web site https://fedorahosted.org/elections.
* Licence
+
fedora-elections is licenced under GPL v2.
9 years, 10 months
356 commits - alembic/env.py alembic/script.py.mako alembic/versions AUTHORS COPYING createdb.py development.ini elections2/config elections2/controllers elections2/i18n elections2/__init__.py elections2/lib elections2/model elections2/public elections2/templates elections2/tests elections2/websetup elections.sql fedmsg.d/elections.py fedora_elections/admin.py fedora_elections/default_config.py fedora_elections/elections.py fedora_elections/fedmsgshim.py fedora_elections/forms.py fedora_el
by Pierre-YvesChibon
.gitignore | 11
AUTHORS | 4
COPYING | 171 -
MANIFEST.in | 12
README | 122 -
README.txt | 44
TODO | 32
UPDATE-NOTES | 1
alembic/env.py | 72
alembic/script.py.mako | 22
alembic/versions/d07c5ef2d03_add_max_votes_to_ele.py | 28
createdb.py | 7
dev/null |binary
development.ini | 135 -
elections.sql | 90
elections2/__init__.py | 2
elections2/config/__init__.py | 2
elections2/config/app_cfg.py | 75
elections2/config/deployment.ini | 99 -
elections2/config/deployment.ini_tmpl | 98 -
elections2/config/environment.py | 9
elections2/config/middleware.py | 39
elections2/controllers/__init__.py | 2
elections2/controllers/controller.template | 23
elections2/controllers/error.py | 30
elections2/controllers/root.py | 284 ---
elections2/controllers/secure.py | 31
elections2/controllers/template.py | 36
elections2/i18n/ru/LC_MESSAGES/elections2.po | 24
elections2/lib/__init__.py | 2
elections2/lib/admin.py | 273 ---
elections2/lib/app_globals.py | 18
elections2/lib/base.py | 32
elections2/lib/helpers.py | 5
elections2/lib/vote.py | 198 --
elections2/model/__init__.py | 189 --
elections2/model/auth.py | 227 --
elections2/model/model.template | 22
elections2/public/css/style.css | 57
elections2/templates/about.html | 50
elections2/templates/admedit.html | 74
elections2/templates/admin.html | 37
elections2/templates/admnewc.html | 19
elections2/templates/admnewe.html | 27
elections2/templates/error.html | 19
elections2/templates/fedora_login.html | 105 -
elections2/templates/index.html | 113 -
elections2/templates/login.html | 20
elections2/templates/master.html | 81
elections2/templates/results.html | 31
elections2/templates/verify.html | 30
elections2/templates/vote.html | 61
elections2/tests/__init__.py | 64
elections2/tests/functional/__init__.py | 2
elections2/tests/functional/test_authentication.py | 85
elections2/tests/functional/test_root.py | 80
elections2/tests/models/__init__.py | 62
elections2/tests/models/test_auth.py | 51
elections2/websetup/__init__.py | 19
elections2/websetup/bootstrap.py | 57
elections2/websetup/schema.py | 21
fedmsg.d/elections.py | 20
fedora-elections.spec | 80
fedora_elections/__init__.py | 276 +++
fedora_elections/admin.py | 393 ++++
fedora_elections/default_config.py | 13
fedora_elections/elections.py | 354 +++
fedora_elections/fedmsgshim.py | 17
fedora_elections/forms.py | 176 +
fedora_elections/models.py | 417 ++++
fedora_elections/proxy.py | 65
fedora_elections/static/elections-logo.png |binary
fedora_elections/static/elections.css | 219 ++
fedora_elections/static/header-bg.png |binary
fedora_elections/static/images/ui-bg_flat_0_aaaaaa_40x100.png |binary
fedora_elections/static/images/ui-bg_flat_0_eeeeee_40x100.png |binary
fedora_elections/static/images/ui-bg_flat_55_ffffff_40x100.png |binary
fedora_elections/static/images/ui-bg_flat_75_ffffff_40x100.png |binary
fedora_elections/static/images/ui-bg_glass_65_ffffff_1x400.png |binary
fedora_elections/static/images/ui-bg_highlight-soft_100_f6f6f6_1x100.png |binary
fedora_elections/static/images/ui-bg_highlight-soft_25_3c6eb4_1x100.png |binary
fedora_elections/static/images/ui-bg_highlight-soft_50_dddddd_1x100.png |binary
fedora_elections/static/images/ui-bg_highlight-soft_75_dddddd_1x100.png |binary
fedora_elections/static/images/ui-icons_3c6eb4_256x240.png |binary
fedora_elections/static/images/ui-icons_454545_256x240.png |binary
fedora_elections/static/images/ui-icons_666666_256x240.png |binary
fedora_elections/static/images/ui-icons_db3279_256x240.png |binary
fedora_elections/static/images/ui-icons_ffffff_256x240.png |binary
fedora_elections/static/jquery-1.10.2.min.js | 6
fedora_elections/static/jquery-ui-1.9.2.custom.css | 416 ++++
fedora_elections/static/jquery-ui-1.9.2.custom.min.js | 6
fedora_elections/static/koji.css | 502 +++++
fedora_elections/static/style.css | 144 +
fedora_elections/templates/_formhelpers.html | 51
fedora_elections/templates/about.html | 40
fedora_elections/templates/admin/all_elections.html | 42
fedora_elections/templates/admin/candidate.html | 25
fedora_elections/templates/admin/candidate_multi.html | 29
fedora_elections/templates/admin/delete_candidate.html | 22
fedora_elections/templates/admin/election_form.html | 53
fedora_elections/templates/admin/view_election.html | 52
fedora_elections/templates/archive.html | 29
fedora_elections/templates/base.html | 96 +
fedora_elections/templates/index.html | 66
fedora_elections/templates/results.html | 93 +
fedora_elections/templates/vote_range.html | 99 +
fedora_elections/templates/vote_simple.html | 46
files/alembic.ini | 54
files/fedora-elections.cfg | 28
files/fedora-elections.conf | 27
files/fedora-elections.spec | 161 +
files/fedora-elections.wsgi | 19
files/update_1_to_2.sql | 25
nosetests | 10
requirements.txt | 11
runserver.py | 8
runtests.sh | 4
setup.cfg | 29
setup.py | 67
test.ini | 26
tests/__init__.py | 181 +
tests/test_candidate.py | 202 ++
tests/test_election.py | 287 +++
tests/test_flask.py | 312 +++
tests/test_flask_admin.py | 906 ++++++++++
tests/test_flask_elections.py | 682 +++++++
tests/test_vote.py | 214 ++
127 files changed, 7266 insertions(+), 3470 deletions(-)
New commits:
commit 69180544fc1c6ad1d0e062c419cdf8e6a9c35f5f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 19:21:09 2014 +0200
Release 2.2
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 4af62f1..103cade 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -24,7 +24,7 @@
# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
-__version__ = '2.1.1'
+__version__ = '2.2'
import os
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index b4976a5..221b519 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -1,8 +1,8 @@
%define modname fedora_elections
Name: fedora-elections
-Version: 2.1.1
-Release: 2%{?dist}
+Version: 2.2
+Release: 1%{?dist}
Summary: Fedora elections application
Group: Development/Languages
@@ -109,6 +109,17 @@ install -m 644 files/update_1_to_2.sql \
%changelog
+* Thu Jul 10 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.2-1
+- Update to 2.2
+- Better description of the different types of election
+- Better error message from Chaoyi Zha
+- Fix FAS integration for range voting and simple voting
+- Update the logic to rely on candidate id for the field.short_name instead
+ of the candidate name as this was causing problem when the candidate name
+ had accents in it
+- Run the unit-tests against faitout only when running in jenkins
+- Adjust and improve the unit-tests suite
+
* Wed Jun 18 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.1.1-2
- Try running unit-tests in the %%check section
- Adjust the dependencies to include what's needed to run the tests
commit 35ed15d1808a0f546511de4d339e34aa7ae22474
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 19:10:55 2014 +0200
Expand the unit-tests to include a test w/ no max_votes set upon creation
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index d7963e1..2aa64a3 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -291,6 +291,46 @@ class FlaskAdmintests(ModelFlasktests):
self.assertTrue(
'<li>Legal voters: packager, testers</li>'
in output.data)
+ self.assertTrue(
+ '<li>Maximum number of votes: None</li>'
+ in output.data)
+
+ # All good - max_votes is ignored as it is not a integer
+ data = {
+ 'alias': 'new_election2',
+ 'shortdesc': 'new election2 shortdesc',
+ 'description': 'new election2 description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=2),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': 2,
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ 'admin_grp': 'testers, , sysadmin-main,,',
+ 'lgl_voters': 'testers, packager,,,',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/new', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Election "new_election2" added</li>'
+ in output.data)
+ self.assertTrue(
+ '<a href="/admin/new_election2/edit">' in output.data)
+ self.assertTrue(
+ '<p>There are no candidates.</p>' in output.data)
+ self.assertTrue(
+ '<li>Admin groups: sysadmin-main, testers</li>'
+ in output.data)
+ self.assertTrue(
+ '<li>Legal voters: packager, testers</li>'
+ in output.data)
+ self.assertTrue(
+ '<li>Maximum number of votes: None</li>'
+ in output.data)
def test_admin_edit_election(self):
""" Test the admin_edit_election function. """
commit e24ff00549904b69003068c4b84f651eb2b65f5e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 19:08:53 2014 +0200
Fix adding voter groups w/o spaces in their name
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index 1de6a3f..dd1abb0 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -110,7 +110,7 @@ def admin_new_election():
if voter_grp.strip():
lglvoters = models.LegalVoter(
election=election,
- group_name=voter_grp
+ group_name=voter_grp.strip(),
)
SESSION.add(lglvoters)
commit 908ba750f772a0d43cd156259033037a1d98b4f3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 19:03:53 2014 +0200
Expand the coverage to test for max_votes not being an int and adding lgl_voters groups when creating the election
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index 1f26757..d7963e1 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -257,7 +257,7 @@ class FlaskAdmintests(ModelFlasktests):
'<td class="error">There is already another election with '
'this summary.</td>' in output.data)
- # All good
+ # All good - max_votes is ignored as it is not a integer
data = {
'alias': 'new_election',
'shortdesc': 'new election shortdesc',
@@ -270,6 +270,8 @@ class FlaskAdmintests(ModelFlasktests):
'candidates_are_fasusers': False,
'embargoed': True,
'admin_grp': 'testers, sysadmin-main,,',
+ 'lgl_voters': 'testers, packager,,',
+ 'max_votes': 'wrong',
'csrf_token': csrf_token,
}
@@ -286,6 +288,9 @@ class FlaskAdmintests(ModelFlasktests):
self.assertTrue(
'<li>Admin groups: sysadmin-main, testers</li>'
in output.data)
+ self.assertTrue(
+ '<li>Legal voters: packager, testers</li>'
+ in output.data)
def test_admin_edit_election(self):
""" Test the admin_edit_election function. """
@@ -373,6 +378,7 @@ class FlaskAdmintests(ModelFlasktests):
'seats_elected': '2',
'candidates_are_fasusers': False,
'embargoed': False,
+ 'max_votes': 'wrong',
'csrf_token': csrf_token,
}
commit 24cd0e5b638ad39eb3d330a4d1a2fee1a5392718
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:56:57 2014 +0200
Adjust the unit-tests now that the range voting relies on candidate identifier rather than their name
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index d316ba7..abda30e 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -197,9 +197,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid candidate id
data = {
- '9': 1,
- '5': 3,
- '6': 2,
+ 'Toshio': 1,
+ 'kevin': 3,
+ 'Ralph': 2,
'action': 'preview',
}
@@ -214,9 +214,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid candidate id
data = {
- '9': 1,
- '5': 3,
- '6': 2,
+ 'Toshio': 1,
+ 'Kevin': 3,
+ 'Ralph': 2,
'action': 'preview',
'csrf_token': csrf_token,
}
@@ -229,9 +229,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid vote: too low
data = {
- 'Toshio': -1,
- 'Kevin': 3,
- 'Ralph': 2,
+ '9': -1,
+ '6': 3,
+ '5': 2,
'action': 'preview',
'csrf_token': csrf_token,
}
@@ -249,9 +249,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid vote: too high
data = {
- 'Toshio': 5,
- 'Kevin': 3,
- 'Ralph': 2,
+ '9': 5,
+ '6': 3,
+ '5': 2,
'action': 'preview',
'csrf_token': csrf_token,
}
@@ -269,9 +269,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid vote: Not numeric
data = {
- 'Toshio': 'a',
- 'Kevin': 3,
- 'Ralph': 2,
+ '9': 'a',
+ '6': 3,
+ '5': 2,
'action': 'preview',
'csrf_token': csrf_token,
}
@@ -289,9 +289,9 @@ class FlaskElectionstests(ModelFlasktests):
# Valid input
data = {
- 'Toshio': 1,
- 'Kevin': 3,
- 'Ralph': 2,
+ '4': 1,
+ '6': 3,
+ '5': 2,
'action': 'preview',
'csrf_token': csrf_token,
}
@@ -322,9 +322,9 @@ class FlaskElectionstests(ModelFlasktests):
with user_set(fedora_elections.APP, user):
# No csrf token provided
data = {
- '9': 1,
- '5': 3,
- '6': 2,
+ 'Toshio': 1,
+ 'Kevin': 3,
+ 'Ralph': 2,
'action': 'submit',
}
@@ -339,9 +339,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid vote: too low
data = {
- 'Toshio': -1,
- 'Kevin': 3,
- 'Ralph': 2,
+ '4': -1,
+ '5': 3,
+ '6': 2,
'action': 'submit',
'csrf_token': csrf_token,
}
@@ -355,9 +355,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid vote: too high
data = {
- 'Toshio': 5,
- 'Kevin': 3,
- 'Ralph': 2,
+ '4': 5,
+ '5': 3,
+ '6': 2,
'action': 'submit',
'csrf_token': csrf_token,
}
@@ -371,9 +371,9 @@ class FlaskElectionstests(ModelFlasktests):
# Invalid vote: Not numeric
data = {
- 'Toshio': 'a',
- 'Kevin': 3,
- 'Ralph': 2,
+ '4': 'a',
+ '5': 3,
+ '6': 2,
'action': 'submit',
'csrf_token': csrf_token,
}
@@ -387,9 +387,9 @@ class FlaskElectionstests(ModelFlasktests):
# Valid input
data = {
- 'Toshio': 1,
- 'Kevin': 3,
- 'Ralph': 2,
+ '4': 1,
+ '5': 3,
+ '6': 2,
'action': 'submit',
'csrf_token': csrf_token,
}
commit 1df6483a53869846e74b95406a00bb83e5b64eb0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:56:26 2014 +0200
Adjust unit-tests for the archive page
diff --git a/tests/test_flask.py b/tests/test_flask.py
index 1ff7efe..f05a563 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -255,9 +255,9 @@ class Flasktests(ModelFlasktests):
self.assertTrue('<title>Fedora elections</title>' in output.data)
self.assertTrue('<h2>Past Elections</h2>' in output.data)
self.assertTrue(
- '<td>test election 2 description</td>' in output.data)
+ '<td>test election 2 shortdesc</td>' in output.data)
self.assertTrue(
- '<td>test election description</td>' in output.data)
+ '<td>test election shortdesc</td>' in output.data)
self.assertEqual(output.data.count('<a href="results/'), 2)
self.assertEqual(output.data.count('<a href="about/'), 2)
self.assertTrue('<a href="/login">login</a>' in output.data)
commit 5796530db4d7691f59331bece5c0452ce6fd3294
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:55:42 2014 +0200
Fix the range voting to be sure the user is not voting on a candidate of another election
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 6ae6793..9a3fcd7 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -129,9 +129,7 @@ def vote_range(election):
num_candidates = election.candidates.count()
- cand_name = {}
- for candidate in election.candidates:
- cand_name[candidate.name] = candidate.id
+ cand_ids = [str(cand.id) for cand in election.candidates]
next_action = 'confirm'
max_selection = num_candidates
@@ -149,11 +147,15 @@ def vote_range(election):
if candidate.short_name in ['csrf_token', 'action']:
continue
+ if candidate.short_name not in cand_ids:
+ flask.flash('Invalid input submitted', 'error')
+ return safe_redirect_back()
+
new_vote = models.Vote(
election_id=election.id,
voter=flask.g.fas_user.username,
timestamp=datetime.now(),
- candidate_id=cand_name[candidate.short_name],
+ candidate_id=candidate.short_name,
value=candidate.data,
)
SESSION.add(new_vote)
commit e7f4a9545378c32b53f37957c3208322835bef1e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:38:19 2014 +0200
Only run the unit-tests against faitout when the tests are ran in jenkins
diff --git a/tests/__init__.py b/tests/__init__.py
index 2881e5d..08c6833 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -50,14 +50,15 @@ from fedora_elections import models
DB_PATH = 'sqlite:///:memory:'
FAITOUT_URL = 'http://209.132.184.152/faitout/'
-try:
- import requests
- req = requests.get('%s/new' % FAITOUT_URL)
- if req.status_code == 200:
- DB_PATH = req.text
- print 'Using faitout at: %s' % DB_PATH
-except:
- pass
+if os.environ.get('BUILD_ID'):
+ try:
+ import requests
+ req = requests.get('%s/new' % FAITOUT_URL)
+ if req.status_code == 200:
+ DB_PATH = req.text
+ print 'Using faitout at: %s' % DB_PATH
+ except:
+ pass
TODAY = date.today()
commit f0f089217dbb6e23770477fd79fe562882dc54ef
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:37:51 2014 +0200
Fix the situation where the candidate.name has accent by using the candidate identifier
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index beee314..6ae6793 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -170,12 +170,12 @@ def vote_range(election):
if (election.candidates_are_fasusers): # pragma: no cover
for candidate in election.candidates:
try:
- usernamemap[candidate.name] = \
+ usernamemap[str(candidate.id)] = \
FAS2.person_by_username(candidate.name)['human_name']
except (KeyError, AuthError), err:
APP.logger.debug(err)
# User has their name set to private or user doesn't exist.
- usernamemap[candidate.name] = candidate.name
+ usernamemap[str(candidate.id)] = candidate.name
return flask.render_template(
'vote_range.html',
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 8dadd58..345056e 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -128,7 +128,7 @@ def get_range_voting_form(candidates, max_range):
title,
choices=[(str(item), item) for item in range(max_range + 1)]
)
- setattr(RangeVoting, candidate.name, field)
+ setattr(RangeVoting, str(candidate.id), field)
return RangeVoting()
commit 4a09461e316893c97b4571bd01601a4a196fe5a9
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:26:26 2014 +0200
For single vote election, do the FAS integration when building the form
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 6ab41e6..beee314 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -269,7 +269,8 @@ def vote_simple(election):
next_action = 'confirm'
form = forms.get_simple_voting_form(
- candidates=election.candidates)
+ candidates=election.candidates,
+ fasusers=election.candidates_are_fasusers)
if form.validate_on_submit():
if form.action.data == 'submit':
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 7d9d508..8dadd58 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -4,7 +4,8 @@ import flask
import wtforms
from flask.ext import wtf
-from fedora_elections import SESSION
+from fedora.client import AuthError
+from fedora_elections import SESSION, FAS2
from fedora_elections.models import Election
@@ -132,13 +133,19 @@ def get_range_voting_form(candidates, max_range):
return RangeVoting()
-def get_simple_voting_form(candidates):
+def get_simple_voting_form(candidates, fasusers):
class SimpleVoting(wtf.Form):
action = wtforms.HiddenField()
titles = []
for candidate in candidates:
title = candidate.name
+ if fasusers:
+ try:
+ title = \
+ FAS2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError), err:
+ APP.logger.debug(err)
if candidate.url:
title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
titles.append((str(candidate.id), title))
commit 105860efe9d50b0346fa9a7bf4ee38072a81f948
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:22:30 2014 +0200
Drop the usernamemap from the single vote election type
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 89fae07..6ab41e6 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -294,25 +294,11 @@ def vote_simple(election):
flask.flash("Please confirm your vote!")
next_action = 'vote'
- usernamemap = {}
- for candidate in election.candidates:
- if (election.candidates_are_fasusers): # pragma: no cover
- try:
- usernamemap[candidate.name] = \
- FAS2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError), err:
- APP.logger.debug(err)
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.name] = candidate.name
- else:
- usernamemap[candidate.name] = candidate.name
-
return flask.render_template(
'vote_simple.html',
election=election,
form=form,
num_candidates=num_candidates,
- usernamemap=usernamemap,
nextaction=next_action)
commit a6c1b672e76d36135d4a34c00429c0d7c0efa124
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:17:53 2014 +0200
Forward the usernamemap to the render_field_in_row macro to display the FAS user name correctly
diff --git a/fedora_elections/templates/vote_range.html b/fedora_elections/templates/vote_range.html
index 62bcc01..141259e 100644
--- a/fedora_elections/templates/vote_range.html
+++ b/fedora_elections/templates/vote_range.html
@@ -15,7 +15,7 @@
{{ render_field_data_in_row(
field, num_candidates=num_candidates, usernamemap=usernamemap) }}
{% else %}
- {{ render_field_in_row(field) }}
+ {{ render_field_in_row(field, usernamemap=usernamemap) }}
{% endif %}
{% endfor %}
commit 31e3aa70b2fa4988b4d9a2d6e6e9a0f27502b05c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:17:31 2014 +0200
Use the shortdesc rather than the description in the archive list/page
diff --git a/fedora_elections/templates/archive.html b/fedora_elections/templates/archive.html
index f0d539a..ec46f15 100644
--- a/fedora_elections/templates/archive.html
+++ b/fedora_elections/templates/archive.html
@@ -14,7 +14,7 @@
</tr>
{% for election in elections %}
<tr>
- <td>{{election.description}}</td>
+ <td>{{election.shortdesc}}</td>
<td>{{election.start_date}}</td>
<td>{{election.end_date}}</td>
<td>
commit 48ab15ec05cd79ef924cc6c7b974de4eced10cc3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:17:14 2014 +0200
Fix displaying the FAS name when the FAS integration is activated
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
index 4fea338..81c0abb 100644
--- a/fedora_elections/templates/_formhelpers.html
+++ b/fedora_elections/templates/_formhelpers.html
@@ -13,7 +13,7 @@
{% macro render_field_in_row(field, usernamemap=None, after="") %}
<tr>
- <td>{% if usernamema %} {{ usernamemap[field.label] }} {%
+ <td>{% if usernamemap %} {{ usernamemap[field.name] }} {%
else %} {{ field.label }} {% endif %}</td>
<td>{{ field(**kwargs)|safe }}</td>
{% if after %} <td>{{ after }}</td>{% endif %}
@@ -24,7 +24,7 @@
{% macro render_field_data_in_row(field, num_candidates, usernamemap=None, after="") %}
<tr>
- <td>{% if usernamema %} {{ usernamemap[field.label] }} {%
+ <td>{% if usernamemap %} {{ usernamemap[field.name] }} {%
else %} {{ field.label }} {% endif %}</td>
<td>
{% set weightpercent = (field.data | int *100) / num_candidates %}
commit 4c6d180471a75aea462e367d85dcf56a50e5f9e5
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:16:19 2014 +0200
Fix integrating candidate name with FAS when the election is set as such
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 1a6731c..89fae07 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -170,11 +170,12 @@ def vote_range(election):
if (election.candidates_are_fasusers): # pragma: no cover
for candidate in election.candidates:
try:
- usernamemap[candidate.id] = \
+ usernamemap[candidate.name] = \
FAS2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
+ except (KeyError, AuthError), err:
+ APP.logger.debug(err)
# User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
+ usernamemap[candidate.name] = candidate.name
return flask.render_template(
'vote_range.html',
@@ -242,11 +243,12 @@ def vote_select(election):
if (election.candidates_are_fasusers): # pragma: no cover
for candidate in election.candidates:
try:
- usernamemap[candidate.id] = \
+ usernamemap[candidate.name] = \
FAS2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
+ except (KeyError, AuthError), err:
+ APP.logger.debug(err)
# User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
+ usernamemap[candidate.name] = candidate.name
return flask.render_template(
'vote_simple.html',
@@ -298,7 +300,8 @@ def vote_simple(election):
try:
usernamemap[candidate.name] = \
FAS2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
+ except (KeyError, AuthError), err:
+ APP.logger.debug(err)
# User has their name set to private or user doesn't exist.
usernamemap[candidate.name] = candidate.name
else:
commit be46abe0c331bbe656495820deff0da1a7ce895b
Author: Chaoyi Zha <summermontreal(a)gmail.com>
Date: Sun Jun 22 07:32:44 2014 -0400
fix unit tests
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index d649f5e..d316ba7 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -672,8 +672,8 @@ class FlaskElectionstests(ModelFlasktests):
'/results/test_election3', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<li class="message">Sorry but this election is in progress, '
- 'you may not see its results already.</li>' in output.data)
+ '<li class="message">Sorry but this election is in progress,'
+ ' and you may not see its results yet.</li>' in output.data)
self.assertTrue('<h3>Current elections</h3>' in output.data)
commit 1994871e92a5289c531c3ad7f1eec65eff85ec23
Author: Chaoyi Zha <summermontreal(a)gmail.com>
Date: Sun Jun 22 07:17:31 2014 -0400
small typo
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index d774c7a..1a6731c 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -60,7 +60,7 @@ def login_required(f):
def get_valid_election(election_alias, ended=False):
- """ Return the election if it is valid (no pending, not ended).
+ """ Return the election if it is valid (not pending, not ended).
"""
election = models.Election.get(SESSION, alias=election_alias)
@@ -81,8 +81,8 @@ def get_valid_election(election_alias, ended=False):
elif ended and election.status == 'In progress':
flask.flash(
- 'Sorry but this election is in progress, you may not see its '
- 'results already.')
+ 'Sorry but this election is in progress, and you may not see its '
+ 'results yet.')
return safe_redirect_back()
return election
commit 408e3e94a2843e3e7638f54119f5184dc26a7f66
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sun Jun 22 10:28:36 2014 +0200
Prepare running the unit-tests at build time but cannot be activated for the moment
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index bd6ba44..b4976a5 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -2,7 +2,7 @@
Name: fedora-elections
Version: 2.1.1
-Release: 1%{?dist}
+Release: 2%{?dist}
Summary: Fedora elections application
Group: Development/Languages
@@ -22,6 +22,8 @@ BuildRequires: python-openid-teams
BuildRequires: python-openid-cla
BuildRequires: python-setuptools
BuildRequires: python-wtforms
+BuildRequires: python-nose
+BuildRequires: python-coverage
Requires: python-fedora
Requires: python-fedora-flask
@@ -89,11 +91,10 @@ cp -r alembic \
install -m 644 files/update_1_to_2.sql \
$RPM_BUILD_ROOT/%{_datadir}/fedora-elections/update_1_to_2.sql
-
-
-%clean
-rm -rf $RPM_BUILD_ROOT
-
+## Try running the unit-tests at build time but this requires flask 0.10+ which
+## is not present in elep6
+#%check
+#./runtests.sh -v -x
%files
%defattr(-,root,root,-)
@@ -108,6 +109,10 @@ rm -rf $RPM_BUILD_ROOT
%changelog
+* Wed Jun 18 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.1.1-2
+- Try running unit-tests in the %%check section
+- Adjust the dependencies to include what's needed to run the tests
+
* Tue Jun 10 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.1.1-1
- Update to 2.1.1
- Fix FAS integration (missing imports)
@@ -138,7 +143,7 @@ rm -rf $RPM_BUILD_ROOT
- Remove instanciated a sqlite database
- Install the sql upgrade script from elections v1 to elections v2
-* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 2-1
+* Tue Sep 03 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 2-1
- Simple voting
* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 2-0
commit 5d75608c4adbe7a93537cd570e880a1ab928f917
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sun Jun 22 10:26:47 2014 +0200
Include the runtest.sh and nosetests scripts in the releases
diff --git a/MANIFEST.in b/MANIFEST.in
index daf6963..c38b898 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,5 @@
include AUTHORS COPYING README requirements.txt
-include createdb.py
+include createdb.py runtests.sh nosetests
recursive-include fedora_elections *
recursive-include files *
recursive-include tests *
commit 4212f2df81357650291789953474a7c5949812b2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sun Jun 22 09:48:45 2014 +0200
Improve again the description of select voting
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 5b47c1f..7d9d508 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -34,7 +34,8 @@ class ElectionForm(wtf.Form):
('range', 'Range Voting'),
('simple', 'Simple Voting (choose one candidate in the list)'),
('range_3', 'Simplified Range Voting (max is set below)'),
- ('select', 'Select Voting (check or not the box, maximum vote set below)'),
+ ('select', 'Select Voting (checkboxes for each candidate, '
+ 'maximum number of votes set below)'),
],
default='range')
commit db9d859429121279a99f616076e9bb1d51f99490
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 18:43:26 2014 +0200
Improve the description of the different election types
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index bc13180..5b47c1f 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -32,9 +32,9 @@ class ElectionForm(wtf.Form):
'Type<span class="error">*</span>',
choices=[
('range', 'Range Voting'),
- ('simple', 'Simple Voting'),
- ('range_3', 'Simplified Range Voting (max = 3)'),
- ('select', 'Select Voting'),
+ ('simple', 'Simple Voting (choose one candidate in the list)'),
+ ('range_3', 'Simplified Range Voting (max is set below)'),
+ ('select', 'Select Voting (check or not the box, maximum vote set below)'),
],
default='range')
commit de24c84d3513c6605ddf8f984609b2b118ea46cf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 14:12:15 2014 +0200
Bump to 2.1.1
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 05ad731..4af62f1 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -24,7 +24,7 @@
# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
-__version__ = '2.1'
+__version__ = '2.1.1'
import os
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index f178c1d..bd6ba44 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -1,7 +1,7 @@
%define modname fedora_elections
Name: fedora-elections
-Version: 2.1
+Version: 2.1.1
Release: 1%{?dist}
Summary: Fedora elections application
@@ -108,6 +108,10 @@ rm -rf $RPM_BUILD_ROOT
%changelog
+* Tue Jun 10 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.1.1-1
+- Update to 2.1.1
+- Fix FAS integration (missing imports)
+
* Tue Jun 10 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.1-1
- Update to 2.1
- Add simplified range voting
commit 87f076f8bcc8906156adca659fd7e4c8ec42759f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 14:10:51 2014 +0200
Adjust the 1_to_2.sql file for 2.1+
diff --git a/files/update_1_to_2.sql b/files/update_1_to_2.sql
index 79bb69c..251886b 100644
--- a/files/update_1_to_2.sql
+++ b/files/update_1_to_2.sql
@@ -16,6 +16,10 @@ ALTER TABLE elections DROP COLUMN votes_per_user;
-- ALTER TABLE elections RENAME COLUMN shortdesc TO summary;
+
+-- For 2.1
ALTER TABLE elections DROP COLUMN candidates_are_fasusers;
ALTER TABLE elections RENAME COLUMN usefas TO candidates_are_fasusers;
+
+ALTER TABLE elections ALTER COLUMN candidates_are_fasusers TYPE integer;
commit e847389e4c5d11ff73b3ca9a8c0423ad9b5834bc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 14:10:37 2014 +0200
Fix missing imports
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 132304b..d774c7a 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -28,12 +28,13 @@ from datetime import datetime, time
from functools import wraps
import flask
+from fedora.client import AuthError
from fedora_elections import fedmsgshim
from fedora_elections import forms
from fedora_elections import models
from fedora_elections import (
- APP, SESSION, is_authenticated, is_admin, is_election_admin,
+ APP, SESSION, FAS2, is_authenticated, is_admin, is_election_admin,
is_safe_url, safe_redirect_back,
)
commit d1a2564480460fd821b704313d333cc954c4394a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 13:00:41 2014 +0200
Bump to 2.1
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index abb74a4..05ad731 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -24,7 +24,7 @@
# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
-__version__ = '2.0.2'
+__version__ = '2.1'
import os
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index fb45c54..f178c1d 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -1,7 +1,7 @@
%define modname fedora_elections
Name: fedora-elections
-Version: 2.0.2
+Version: 2.1
Release: 1%{?dist}
Summary: Fedora elections application
@@ -108,6 +108,19 @@ rm -rf $RPM_BUILD_ROOT
%changelog
+* Tue Jun 10 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.1-1
+- Update to 2.1
+- Add simplified range voting
+- Add select voting
+- Add group admins
+- Add legal voters
+- Add csrf protections to the voting page
+- Decorate the result page to actually show the result/selection
+- Put all the votes at the same URL
+- Fix using https everywhere
+- fedmsg integration upon election/candidate creation, edition, deletion
+- unit-tests
+
* Sat May 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.0.2-1
- Update to 2.0.2
- Fix is_safe_url method, imports were different here
commit 5031994c546dc3a03b5a606a0dba4ae47e21f6b6
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 13:04:46 2014 +0200
Install the sample/default alembic.ini file as no-replace
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index 366cdbb..fb45c54 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -77,6 +77,11 @@ install -m 644 files/fedora-elections.wsgi \
install -m 644 createdb.py \
$RPM_BUILD_ROOT/%{_datadir}/fedora-elections/fedora-elections_createdb.py
+# Install the alembic configuration file
+mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/fedora-elections
+install -m 644 files/alembic.ini \
+ $RPM_BUILD_ROOT/%{_sysconfdir}/fedora-elections/alembic.ini
+
# Install the alembic files
cp -r alembic \
$RPM_BUILD_ROOT/%{_datadir}/fedora-elections/alembic
@@ -95,6 +100,7 @@ rm -rf $RPM_BUILD_ROOT
%doc
%dir %{_sysconfdir}/%{name}
%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
+%config(noreplace) %{_sysconfdir}/%{name}/alembic.ini
%config(noreplace) %{_sysconfdir}/httpd/conf.d/%{name}.conf
%{python_sitelib}/%{modname}/
%{python_sitelib}/%{modname}*.egg-info/
commit 07ca32cb8ad45ce722e5f61181a0df4b03e1dc9d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 13:03:56 2014 +0200
Install the alembic files in the spec
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index f3598a7..366cdbb 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -77,6 +77,10 @@ install -m 644 files/fedora-elections.wsgi \
install -m 644 createdb.py \
$RPM_BUILD_ROOT/%{_datadir}/fedora-elections/fedora-elections_createdb.py
+# Install the alembic files
+cp -r alembic \
+ $RPM_BUILD_ROOT/%{_datadir}/fedora-elections/alembic
+
install -m 644 files/update_1_to_2.sql \
$RPM_BUILD_ROOT/%{_datadir}/fedora-elections/update_1_to_2.sql
commit 005dfb3b057106a054f88d82799fea8fb22d9f6b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 13:03:04 2014 +0200
Include the alembic files in the sources
diff --git a/MANIFEST.in b/MANIFEST.in
index c6b69a5..daf6963 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,3 +3,4 @@ include createdb.py
recursive-include fedora_elections *
recursive-include files *
recursive-include tests *
+recursive-include alembic *
commit 509bd1fdfe65c1f6519d4d910f07950c605d2b03
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 31 12:55:07 2014 +0200
Take into account max_votes when editing an election
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index af03f95..1de6a3f 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -156,6 +156,14 @@ def admin_edit_election(election_alias):
form = forms.ElectionForm(election.id, obj=election)
if form.validate_on_submit():
form.embargoed.data = int(form.embargoed.data)
+ if form.max_votes.data:
+ try:
+ form.max_votes.data = int(form.max_votes.data)
+ except ValueError:
+ form.max_votes.data = None
+ else:
+ form.max_votes.data = None
+
form.candidates_are_fasusers.data = int(
form.candidates_are_fasusers.data)
form.populate_obj(election)
commit 7b63a48d9e8af4566ac400cbe449240d5c03d180
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 31 12:54:17 2014 +0200
Take the max_votes into account when creating an election
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index 32a4ee6..af03f95 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -67,6 +67,14 @@ def admin_new_election():
form = forms.ElectionForm()
if form.validate_on_submit():
+ if form.max_votes.data:
+ try:
+ form.max_votes.data = int(form.max_votes.data)
+ except ValueError:
+ form.max_votes.data = None
+ else:
+ form.max_votes.data = None
+
election = models.Election(
shortdesc=form.shortdesc.data,
alias=form.alias.data,
@@ -77,6 +85,7 @@ def admin_new_election():
seats_elected=form.seats_elected.data,
embargoed=int(form.embargoed.data),
voting_type=form.voting_type.data,
+ max_votes=form.max_votes.data,
candidates_are_fasusers=int(form.candidates_are_fasusers.data),
fas_user=flask.g.fas_user.username,
)
commit ec40adad5c44daff5471f409ce5eb0cb4413f072
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 18:20:51 2014 +0200
Small pep8 fixes
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index e6726f4..132304b 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -99,8 +99,8 @@ def vote(election_alias):
if len(set(flask.g.fas_user.groups).intersection(
set(election.legal_voters_list))) == 0:
flask.flash(
- 'You are not among the groups that are allowed to vote for this '
- 'election', 'error')
+ 'You are not among the groups that are allowed to vote '
+ 'for this election', 'error')
return safe_redirect_back()
votes = models.Vote.of_user_on_election(
@@ -257,7 +257,6 @@ def vote_select(election):
nextaction=next_action)
-
def vote_simple(election):
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index 5f0402a..1f26757 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -390,7 +390,8 @@ class FlaskAdmintests(ModelFlasktests):
in output.data)
def test_admin_edit_election_admin_groups(self):
- """ Test the admin_edit_election function when editing admin groups. """
+ """ Test the admin_edit_election function when editing admin groups.
+ """
user = FakeUser(
fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
username='toshio')
@@ -474,7 +475,8 @@ class FlaskAdmintests(ModelFlasktests):
in output.data)
def test_admin_edit_election_legal_voters(self):
- """ Test the admin_edit_election function when editing legal voters. """
+ """ Test the admin_edit_election function when editing legal voters.
+ """
user = FakeUser(
fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
username='toshio')
commit dc5ccdfb26da900ece3150759a2cd11b9dcf43cb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:35:50 2014 +0200
Add alembic revision adding max_votes to the elections table
diff --git a/alembic/versions/d07c5ef2d03_add_max_votes_to_ele.py b/alembic/versions/d07c5ef2d03_add_max_votes_to_ele.py
new file mode 100644
index 0000000..4a922ff
--- /dev/null
+++ b/alembic/versions/d07c5ef2d03_add_max_votes_to_ele.py
@@ -0,0 +1,28 @@
+"""Add max_votes to election
+
+
+Revision ID: d07c5ef2d03
+Revises: None
+Create Date: 2014-05-26 16:33:50.812050
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = 'd07c5ef2d03'
+down_revision = None
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ """ Add the max_votes column to the Elections table. """
+ op.add_column(
+ 'elections',
+ sa.Column('max_votes', sa.Integer, nullable=True)
+ )
+
+
+def downgrade():
+ """ Drop the max_votes column from the Elections table. """
+ op.drop_column('elections', 'max_votes')
commit d66dfb962ffb48f61bcaf82894c3044c00a56c76
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:36:18 2014 +0200
Ignore alembic.ini files
diff --git a/.gitignore b/.gitignore
index fc2dc27..3ea936c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@
build/
dist/
.coverage
+alembic.ini
commit e3d8d37dff15aa7911bf4b7c5c4787e5ead72a1b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:35:38 2014 +0200
Add sample alembic.ini file in the files folder
diff --git a/files/alembic.ini b/files/alembic.ini
new file mode 100644
index 0000000..920f5df
--- /dev/null
+++ b/files/alembic.ini
@@ -0,0 +1,54 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+
+# In production
+# script_location = /usr/share/fedora_elections/alembic
+# For development
+script_location = alembic
+
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+sqlalchemy.url = driver://user:pass@localhost/dbname
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
commit 8d9dafefb4fddfa90dade8cb267735fd5aa45ade
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:35:22 2014 +0200
Add basic alembic structure for alembic integration
diff --git a/alembic/env.py b/alembic/env.py
new file mode 100644
index 0000000..a280347
--- /dev/null
+++ b/alembic/env.py
@@ -0,0 +1,72 @@
+from __future__ import with_statement
+from alembic import context
+from sqlalchemy import engine_from_config, pool
+from logging.config import fileConfig
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+target_metadata = None
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(url=url)
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(
+ config.get_section(config.config_ini_section),
+ prefix='sqlalchemy.',
+ poolclass=pool.NullPool)
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata)
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/alembic/script.py.mako b/alembic/script.py.mako
new file mode 100644
index 0000000..9570201
--- /dev/null
+++ b/alembic/script.py.mako
@@ -0,0 +1,22 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision}
+Create Date: ${create_date}
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
commit 3aa17087b1dc9ed982a8fc4714f58253c9ac0c5c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:30:27 2014 +0200
The simple voting form has no use of the maximum votes/range
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 411f609..e6726f4 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -267,8 +267,7 @@ def vote_simple(election):
next_action = 'confirm'
form = forms.get_simple_voting_form(
- candidates=election.candidates,
- max_range=num_candidates)
+ candidates=election.candidates)
if form.validate_on_submit():
if form.action.data == 'submit':
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index c655939..bc13180 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -131,7 +131,7 @@ def get_range_voting_form(candidates, max_range):
return RangeVoting()
-def get_simple_voting_form(candidates, max_range):
+def get_simple_voting_form(candidates):
class SimpleVoting(wtf.Form):
action = wtforms.HiddenField()
commit d75e6b95fa6be67194645d39b67bff20de597fed
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:27:27 2014 +0200
Use the max_votes field of the election to set the maximum ranges or vote
This in the range_voting and select_voting type of elections
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index e3549ad..411f609 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -112,8 +112,6 @@ def vote(election_alias):
if election.voting_type == 'range':
return vote_range(election)
- elif election.voting_type == 'range_3':
- return vote_range(election, max_range=3)
elif election.voting_type == 'simple':
return vote_simple(election)
elif election.voting_type == 'select':
@@ -124,7 +122,7 @@ def vote(election_alias):
return safe_redirect_back()
-def vote_range(election, max_range=None):
+def vote_range(election):
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
@@ -135,12 +133,13 @@ def vote_range(election, max_range=None):
cand_name[candidate.name] = candidate.id
next_action = 'confirm'
- if max_range is None:
- max_range = num_candidates
+ max_selection = num_candidates
+ if election.max_votes:
+ max_selection = election.max_votes
form = forms.get_range_voting_form(
candidates=election.candidates,
- max_range=max_range)
+ max_range=max_selection)
if form.validate_on_submit():
@@ -181,12 +180,12 @@ def vote_range(election, max_range=None):
election=election,
form=form,
num_candidates=num_candidates,
- max_range=max_range,
+ max_range=max_selection,
usernamemap=usernamemap,
nextaction=next_action)
-def vote_select(election, max_selection=None):
+def vote_select(election):
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
@@ -197,8 +196,9 @@ def vote_select(election, max_selection=None):
cand_name[candidate.name] = candidate.id
next_action = 'confirm'
- if max_selection is None:
- max_selection = num_candidates
+ max_selection = num_candidates
+ if election.max_votes:
+ max_selection = election.max_votes
form = forms.get_select_voting_form(
candidates=election.candidates,
@@ -206,27 +206,36 @@ def vote_select(election, max_selection=None):
if form.validate_on_submit():
- if form.action.data == 'submit':
- for candidate in form:
- if candidate.short_name in ['csrf_token', 'action']:
- continue
-
- new_vote = models.Vote(
- election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=cand_name[candidate.short_name],
- value=int(candidate.data),
- )
- SESSION.add(new_vote)
- SESSION.commit()
-
- flask.flash("Your vote has been recorded. Thank you!")
- return safe_redirect_back()
+ cnt = 0
+ for candidate in form:
+ if candidate.short_name in ['csrf_token', 'action']:
+ continue
+ if candidate.data:
+ cnt += 1
+ if cnt > max_selection:
+ flask.flash('Too many candidates submitted', 'error')
+ else:
+ if form.action.data == 'submit':
+ for candidate in form:
+ if candidate.short_name in ['csrf_token', 'action']:
+ continue
+
+ new_vote = models.Vote(
+ election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=cand_name[candidate.short_name],
+ value=int(candidate.data),
+ )
+ SESSION.add(new_vote)
+ SESSION.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return safe_redirect_back()
- if form.action.data == 'preview':
- flask.flash("Please confirm your vote!")
- next_action = 'vote'
+ if form.action.data == 'preview':
+ flask.flash("Please confirm your vote!")
+ next_action = 'vote'
usernamemap = {}
if (election.candidates_are_fasusers): # pragma: no cover
commit 0539a49cb77b19f9415896af23b8424171f2242c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:28:04 2014 +0200
Adjust the edit and view election templates to display the max_votes field/info
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index 050c94d..fceb75a 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -15,6 +15,7 @@
{{ render_field_in_row(form.shortdesc) }}
{{ render_field_in_row(form.description) }}
{{ render_field_in_row(form.voting_type) }}
+ {{ render_field_in_row(form.max_votes) }}
{{ render_field_in_row(form.url) }}
{{ render_field_in_row(form.start_date, " at 00:00:00 UTC") }}
{{ render_field_in_row(form.end_date, " at 23:59:59 UTC") }}
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index 90ab10b..2247754 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -15,6 +15,7 @@
<li>End date: {{ election.end_date }}</li>
<li>Number elected: {{ election.seats_elected }}</li>
<li>Election type: {{ election.voting_type }}</li>
+ <li>Maximum number of votes: {{ election.max_votes }}</li>
<li>Admin groups: {{ election.admin_groups_list | join(', ') }}</li>
<li>Legal voters: {{ election.legal_voters_list | join(', ') }}</li>
</ul>
commit 9759dd604b3ce3734df90dd2e865a94d7180dfaf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:24:24 2014 +0200
Add the max_votes field in the election form
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index dbe01cb..c655939 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -38,6 +38,10 @@ class ElectionForm(wtf.Form):
],
default='range')
+ max_votes = wtforms.TextField(
+ 'Maximum Range/Votes',
+ [wtforms.validators.optional()])
+
url = wtforms.TextField(
'URL<span class="error">*</span>', [
wtforms.validators.Required(),
commit f48421957d6bf2775f7cea73550865a5d5836186
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:24:13 2014 +0200
Add a max_votes field in the election table
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index e125545..e0e5941 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -94,6 +94,7 @@ class Election(BASE):
seats_elected = sa.Column(sa.Integer, nullable=False, default=1)
embargoed = sa.Column(sa.Integer, nullable=False, default=0)
voting_type = sa.Column(sa.Unicode(100), nullable=False, default=u'range')
+ max_votes = sa.Column(sa.Integer, nullable=True)
candidates_are_fasusers = sa.Column(
sa.Integer, nullable=False, default=0)
fas_user = sa.Column(sa.Unicode(50), nullable=False)
commit ba5a54e7fd013ae01a3577cc86e1565d48f771cd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:14:39 2014 +0200
Add the logic for vote select
For this type of vote, the user can simply select the candidates of
interest eventually with a maximum number of choices.
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index b4ebd0e..e3549ad 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -116,6 +116,8 @@ def vote(election_alias):
return vote_range(election, max_range=3)
elif election.voting_type == 'simple':
return vote_simple(election)
+ elif election.voting_type == 'select':
+ return vote_select(election)
else: # pragma: no cover
flask.flash(
'Unknown election voting type: %s' % election.voting_type)
@@ -184,6 +186,69 @@ def vote_range(election, max_range=None):
nextaction=next_action)
+def vote_select(election, max_selection=None):
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, election.id, count=True)
+
+ num_candidates = election.candidates.count()
+
+ cand_name = {}
+ for candidate in election.candidates:
+ cand_name[candidate.name] = candidate.id
+ next_action = 'confirm'
+
+ if max_selection is None:
+ max_selection = num_candidates
+
+ form = forms.get_select_voting_form(
+ candidates=election.candidates,
+ max_selection=max_selection)
+
+ if form.validate_on_submit():
+
+ if form.action.data == 'submit':
+ for candidate in form:
+ if candidate.short_name in ['csrf_token', 'action']:
+ continue
+
+ new_vote = models.Vote(
+ election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=cand_name[candidate.short_name],
+ value=int(candidate.data),
+ )
+ SESSION.add(new_vote)
+ SESSION.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return safe_redirect_back()
+
+ if form.action.data == 'preview':
+ flask.flash("Please confirm your vote!")
+ next_action = 'vote'
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers): # pragma: no cover
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ FAS2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template(
+ 'vote_simple.html',
+ election=election,
+ form=form,
+ num_candidates=num_candidates,
+ max_selection=max_selection,
+ usernamemap=usernamemap,
+ nextaction=next_action)
+
+
+
def vote_simple(election):
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
commit 168209cb9a4a561a9dfec7dfb32dd431a05192d3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:14:17 2014 +0200
Add select voting as an option when creating an election
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 62b8dd1..dbe01cb 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -34,6 +34,7 @@ class ElectionForm(wtf.Form):
('range', 'Range Voting'),
('simple', 'Simple Voting'),
('range_3', 'Simplified Range Voting (max = 3)'),
+ ('select', 'Select Voting'),
],
default='range')
commit 08254717559b51c1dcdba47b01768a6be662b487
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:10:48 2014 +0200
Add a form for SelectVoting
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 1abedca..62b8dd1 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -143,3 +143,21 @@ def get_simple_voting_form(candidates, max_range):
setattr(SimpleVoting, 'candidate', field)
return SimpleVoting()
+
+
+def get_select_voting_form(candidates, max_selection):
+ class SelectVoting(wtf.Form):
+ action = wtforms.HiddenField()
+
+ for candidate in candidates:
+ title = candidate.name
+ if candidate.url:
+ title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
+ field = wtforms.BooleanField(
+ title,
+ )
+ setattr(SelectVoting, candidate.name, field)
+
+ # TODO: See if we can add a form wide validation using max_selection
+
+ return SelectVoting()
commit af5931aed99830bb3c4f14aed3c6622cd5df9edc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 15:36:34 2014 +0200
If the election is of type range_3, call the appropriately vote_range
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 206cf7a..b4ebd0e 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -112,6 +112,8 @@ def vote(election_alias):
if election.voting_type == 'range':
return vote_range(election)
+ elif election.voting_type == 'range_3':
+ return vote_range(election, max_range=3)
elif election.voting_type == 'simple':
return vote_simple(election)
else: # pragma: no cover
commit 8a4311e01351b44945a538ebd4dc75a3d4040699
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 15:35:54 2014 +0200
Offer the simplified range voting (max range = 3) in the options when creating an election
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index c58b5ff..1abedca 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -30,8 +30,11 @@ class ElectionForm(wtf.Form):
voting_type = wtforms.RadioField(
'Type<span class="error">*</span>',
- choices=[('range', 'Range Voting'),
- ('simple', 'Simple Voting')],
+ choices=[
+ ('range', 'Range Voting'),
+ ('simple', 'Simple Voting'),
+ ('range_3', 'Simplified Range Voting (max = 3)'),
+ ],
default='range')
url = wtforms.TextField(
commit af76d0f259730180963d971ec987aaab7307f60d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 15:35:25 2014 +0200
Update the information message according to the type of voting used for this election
diff --git a/fedora_elections/templates/vote_range.html b/fedora_elections/templates/vote_range.html
index e1a003a..62bcc01 100644
--- a/fedora_elections/templates/vote_range.html
+++ b/fedora_elections/templates/vote_range.html
@@ -48,6 +48,8 @@
{% else %}
<p />
{% endif %}
+
+{% if election.voting_type == 'range' %}
<div>
<h2>Information on Range Voting</h2>
<p>
@@ -58,8 +60,31 @@
</p>
<p>
To cast your vote in this election simply select a value between 0 and
- {{num_candidates}} with 0 as 'least or no preference' and
- {{num_candidates}} as 'highest preference'.
+ {{ max_range }} with 0 as 'least or no preference' and
+ {{ max_range }} as 'highest preference'.
+ </p>
+ <p>
+ At the end of the election, the highest ranking candidate(s) are marked
+ as the winners.
+ </p>
+ <p>
+ For more information about Range Voting, visit the
+ <a href="http://rangevoting.org/">Center for Range Voting</a>.
+ </p>
+</div>
+{% elif election.voting_type == 'range_3' %}
+<div>
+ <h2>Information on Simplified Range Voting</h2>
+ <p>
+ Fedora Project has implemented a simplified version of
+ <a href="http://rangevoting.org/">Range Voting</a>
+ for this election, in particular the "Range (score-summing, blanks
+ treated as zero score, no quorum rule)" range voting system.
+ </p>
+ <p>
+ To cast your vote in this election simply select a value between 0 and
+ {{ max_range }} with 0 as 'least or no preference' and
+ {{ max_range }} as 'highest preference'.
</p>
<p>
At the end of the election, the highest ranking candidate(s) are marked
@@ -70,4 +95,5 @@
<a href="http://rangevoting.org/">Center for Range Voting</a>.
</p>
</div>
+{% endif %}
{% endblock %}
commit f40500e996937843800211092575f2ea154ec70f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 15:34:00 2014 +0200
Have the vote_range function accept a max_range argument
This argument is the maximal value the user can give to its vote.
If none is provided, it defaults to the number of candidates which is the
default range voting mechanism
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 9e06957..206cf7a 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -120,7 +120,7 @@ def vote(election_alias):
return safe_redirect_back()
-def vote_range(election):
+def vote_range(election, max_range=None):
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
@@ -131,9 +131,12 @@ def vote_range(election):
cand_name[candidate.name] = candidate.id
next_action = 'confirm'
+ if max_range is None:
+ max_range = num_candidates
+
form = forms.get_range_voting_form(
candidates=election.candidates,
- max_range=num_candidates)
+ max_range=max_range)
if form.validate_on_submit():
@@ -174,6 +177,7 @@ def vote_range(election):
election=election,
form=form,
num_candidates=num_candidates,
+ max_range=max_range,
usernamemap=usernamemap,
nextaction=next_action)
commit e7a015294a3347da0400d8364bcdfbfd75d7b86f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:11:49 2014 +0200
Simple voting directly use the candidate identifier now, adjust the tests accordingly
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index 3478b44..d649f5e 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -495,7 +495,7 @@ class FlaskElectionstests(ModelFlasktests):
# Valid input
data = {
- 'candidate': 'Ralph',
+ 'candidate': 7,
'action': 'preview',
'csrf_token': csrf_token,
}
@@ -588,7 +588,7 @@ class FlaskElectionstests(ModelFlasktests):
# Valid input
data = {
- 'candidate': 'Toshio',
+ 'candidate': 8,
'action': 'submit',
'csrf_token': csrf_token,
}
commit 17a06ea0e636ed52c898934909870c340cdaed06
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:09:54 2014 +0200
The usernamemap converts the field label (username) for FAS full names
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index d0e1459..9e06957 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -217,13 +217,13 @@ def vote_simple(election):
for candidate in election.candidates:
if (election.candidates_are_fasusers): # pragma: no cover
try:
- usernamemap[candidate.id] = \
+ usernamemap[candidate.name] = \
FAS2.person_by_username(candidate.name)['human_name']
except (KeyError, AuthError):
# User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
+ usernamemap[candidate.name] = candidate.name
else:
- usernamemap[candidate.id] = candidate.name
+ usernamemap[candidate.name] = candidate.name
return flask.render_template(
'vote_simple.html',
commit 769901de8f54c20a0fa292dfb5443a32b7a5efb2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:09:39 2014 +0200
Make use the usernamemap for range voting
diff --git a/fedora_elections/templates/vote_range.html b/fedora_elections/templates/vote_range.html
index 5e0ff04..e1a003a 100644
--- a/fedora_elections/templates/vote_range.html
+++ b/fedora_elections/templates/vote_range.html
@@ -12,7 +12,8 @@
<table border="1" cellpadding="1">
{% for field in form if field.widget.input_type != 'hidden' %}
{% if nextaction == 'vote' %}
- {{ render_field_data_in_row(field, num_candidates=num_candidates) }}
+ {{ render_field_data_in_row(
+ field, num_candidates=num_candidates, usernamemap=usernamemap) }}
{% else %}
{{ render_field_in_row(field) }}
{% endif %}
commit 44b9c492597fb3e6bf1045c301f9e547aed187be
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:08:34 2014 +0200
Allow to rely on the usernamemap also in render_field_in_row
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
index 24df810..4fea338 100644
--- a/fedora_elections/templates/_formhelpers.html
+++ b/fedora_elections/templates/_formhelpers.html
@@ -11,9 +11,10 @@
</div>
{% endmacro %}
-{% macro render_field_in_row(field, after="") %}
+{% macro render_field_in_row(field, usernamemap=None, after="") %}
<tr>
- <td>{{ field.label }}</td>
+ <td>{% if usernamema %} {{ usernamemap[field.label] }} {%
+ else %} {{ field.label }} {% endif %}</td>
<td>{{ field(**kwargs)|safe }}</td>
{% if after %} <td>{{ after }}</td>{% endif %}
{% if field.errors %}{% for error in field.errors
commit 449256a2b6ba68ec2c4eaa266181577352fb928f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:06:55 2014 +0200
The form now provides the candidate id directly as value (instead of its name before)
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 2c29955..d0e1459 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -200,7 +200,7 @@ def vote_simple(election):
election_id=election.id,
voter=flask.g.fas_user.username,
timestamp=datetime.now(),
- candidate_id=cand_name[candidate.data],
+ candidate_id=candidate.data,
value=1,
)
SESSION.add(new_vote)
commit 0333f0ab84339365fc01219d79c9b522d5f59f64
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:06:34 2014 +0200
Fill in the usernamemap for all the candidates
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index a00201e..2c29955 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -214,14 +214,16 @@ def vote_simple(election):
next_action = 'vote'
usernamemap = {}
- if (election.candidates_are_fasusers): # pragma: no cover
- for candidate in election.candidates:
+ for candidate in election.candidates:
+ if (election.candidates_are_fasusers): # pragma: no cover
try:
usernamemap[candidate.id] = \
FAS2.person_by_username(candidate.name)['human_name']
except (KeyError, AuthError):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
+ else:
+ usernamemap[candidate.id] = candidate.name
return flask.render_template(
'vote_simple.html',
commit ae4093e925848574bef54895a6e5feed8af6fa8d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:06:06 2014 +0200
Drop the cand_name dict as we already have the usernamemap dict
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 3cf40e3..a00201e 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -184,9 +184,6 @@ def vote_simple(election):
num_candidates = election.candidates.count()
- cand_name = {}
- for candidate in election.candidates:
- cand_name[candidate.name] = candidate.id
next_action = 'confirm'
form = forms.get_simple_voting_form(
commit 4102814cecf3649168c511a707b63b3429581f4b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:05:19 2014 +0200
Fixed presenting the fields of the form with their appropriate label/value
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index e4da19f..c58b5ff 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -112,8 +112,8 @@ def get_range_voting_form(candidates, max_range):
for candidate in candidates:
title = candidate.name
- #if candidate.url:
- #title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
+ if candidate.url:
+ title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
field = wtforms.SelectField(
title,
choices=[(str(item), item) for item in range(max_range + 1)]
@@ -130,9 +130,9 @@ def get_simple_voting_form(candidates, max_range):
titles = []
for candidate in candidates:
title = candidate.name
- #if candidate.url:
- #title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
- titles.append((title, title))
+ if candidate.url:
+ title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
+ titles.append((str(candidate.id), title))
field = wtforms.RadioField(
'Candidates',
choices=titles
commit b6ec054f31f93a74f3b086c79a1076923cb36d08
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:04:38 2014 +0200
If there is a usernamemap specified, use it instead of the field label
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
index e0a59b6..24df810 100644
--- a/fedora_elections/templates/_formhelpers.html
+++ b/fedora_elections/templates/_formhelpers.html
@@ -21,9 +21,10 @@
</tr>
{% endmacro %}
-{% macro render_field_data_in_row(field, num_candidates, after="") %}
+{% macro render_field_data_in_row(field, num_candidates, usernamemap=None, after="") %}
<tr>
- <td>{{ field.label }}</td>
+ <td>{% if usernamema %} {{ usernamemap[field.label] }} {%
+ else %} {{ field.label }} {% endif %}</td>
<td>
{% set weightpercent = (field.data | int *100) / num_candidates %}
{% if weightpercent >= 90 %}
commit b7f909ab69a56fb68a99e4beee074edd46aeca9b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:03:57 2014 +0200
No layout change when confirming for the moment
diff --git a/fedora_elections/templates/vote_simple.html b/fedora_elections/templates/vote_simple.html
index 3ebec77..ebc896f 100644
--- a/fedora_elections/templates/vote_simple.html
+++ b/fedora_elections/templates/vote_simple.html
@@ -8,15 +8,10 @@
<p><a href="{{election.url}}">[More Information]</a></p>
-
<form action="{{ url_for('vote', election_alias=election.alias) }}" method="post">
<table border="1" cellpadding="1">
{% for field in form if field.widget.input_type != 'hidden' %}
- {% if nextaction == 'vote' %}
- {{ render_field_data_in_row(field, num_candidates=num_candidates) }}
- {% else %}
{{ render_field_in_row(field) }}
- {% endif %}
{% endfor %}
</table>
commit 5a89394de50f5627833b7f6ddbb7b60e2911f291
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:44:04 2014 +0200
Fix the tests for the change in the simple voting processing and integration of wtforms
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index cbb455b..3478b44 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -152,7 +152,7 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
user = FakeUser(['voters'], username='toshio')
@@ -430,12 +430,12 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
# Invalid vote: No candidate
data = {
- 'name': 'Preview',
+ 'action': 'preview',
}
output = self.app.post('/vote/test_election5', data=data)
@@ -443,16 +443,43 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
self.assertTrue(
- '<li class="message">Please vote for a candidate</li>'
+ '<td class="error">Not a valid choice</td>'
in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
+ in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ # Invalid vote: No candidate
+ data = {
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/vote/test_election5', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<td class="error">Not a valid choice</td>'
+ in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+ self.assertTrue(
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
# Invalid vote: Not numeric
data = {
'candidate': 'a',
- 'name': 'Preview',
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
}
output = self.app.post('/vote/test_election5', data=data)
@@ -460,13 +487,17 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Valid input
data = {
- 'candidate': 7,
- 'name': 'Preview',
+ 'candidate': 'Ralph',
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
}
output = self.app.post('/vote/test_election5', data=data)
@@ -474,7 +505,7 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="confirm" value="Submit" />'
+ '<input type="hidden" name="action" value="submit" />'
in output.data)
self.assertTrue(
'<li class="message">Please confirm your vote!</li>'
@@ -493,51 +524,73 @@ class FlaskElectionstests(ModelFlasktests):
user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
+ # Invalid candidate id - no csrf
+ data = {
+ 'candidate': 1,
+ 'action': 'submit',
+ }
+
+ output = self.app.post(
+ '/vote/test_election5', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
# Invalid candidate id
data = {
'candidate': 1,
- 'name': 'Submit',
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
'/vote/test_election5', data=data,
follow_redirects=True)
self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<li class="error">Invalid vote, this candidate is not '
- 'listed for this election</li>' in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Invalid vote: too low
data = {
'candidate': -1,
- 'name': 'Submit',
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
'/vote/test_election5', data=data,
follow_redirects=True)
self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<li class="error">Invalid vote, this candidate is not '
- 'listed for this election</li>' in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Invalid vote: Not numeric
data = {
'candidate': 'a',
- 'name': 'Submit',
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
'/vote/test_election5', data=data,
follow_redirects=True)
self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<li class="error">Invalid Ballot!</li' in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Valid input
data = {
- 'candidate': 7,
- 'name': 'Submit',
+ 'candidate': 'Toshio',
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
commit ab047e73d58fccdb90f4243ff59434400e7610a9
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:43:33 2014 +0200
Ignore the [more] link for the candidates for the moment, we need a better solution
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index b218468..e4da19f 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -112,8 +112,8 @@ def get_range_voting_form(candidates, max_range):
for candidate in candidates:
title = candidate.name
- if candidate.url:
- title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
+ #if candidate.url:
+ #title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
field = wtforms.SelectField(
title,
choices=[(str(item), item) for item in range(max_range + 1)]
@@ -130,8 +130,8 @@ def get_simple_voting_form(candidates, max_range):
titles = []
for candidate in candidates:
title = candidate.name
- if candidate.url:
- title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
+ #if candidate.url:
+ #title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
titles.append((title, title))
field = wtforms.RadioField(
'Candidates',
commit e33d89fe80dab87f825d0259f582fe3a9ffa97a3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:43:01 2014 +0200
Use candidate instead of candidates to be consistent with the old behavior
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 3e7f7c9..b218468 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -137,6 +137,6 @@ def get_simple_voting_form(candidates, max_range):
'Candidates',
choices=titles
)
- setattr(SimpleVoting, 'candidates', field)
+ setattr(SimpleVoting, 'candidate', field)
return SimpleVoting()
commit b8d48f2e894c0a5f7e8f0e2e01c80fed61080e48
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:30:56 2014 +0200
Adjust the simple voting template to rely on the form just added
diff --git a/fedora_elections/templates/vote_simple.html b/fedora_elections/templates/vote_simple.html
index 77dff56..3ebec77 100644
--- a/fedora_elections/templates/vote_simple.html
+++ b/fedora_elections/templates/vote_simple.html
@@ -1,3 +1,5 @@
+{% from "_formhelpers.html" import render_field_in_row, render_field_data_in_row %}
+
{% extends "base.html" %}
{% block body %}
<h2>{{election.shortdesc}}</h2>
@@ -6,34 +8,30 @@
<p><a href="{{election.url}}">[More Information]</a></p>
+
<form action="{{ url_for('vote', election_alias=election.alias) }}" method="post">
- <table border="0" cellpadding="1">
- {% for candidate in election.candidates|sort %}
- <tr>
- <td>
- {% if candidate.id == candidate_id %}
- <input type="radio" name="candidate" value="{{ candidate.id }}" checked="checked">
- {% else %}
- <input type="radio" name="candidate" value="{{ candidate.id }}">
- {% endif %}
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- <a href="{{candidate.url}}">[Info]</a>
- </td>
- </tr>
+ <table border="1" cellpadding="1">
+ {% for field in form if field.widget.input_type != 'hidden' %}
+ {% if nextaction == 'vote' %}
+ {{ render_field_data_in_row(field, num_candidates=num_candidates) }}
+ {% else %}
+ {{ render_field_in_row(field) }}
+ {% endif %}
{% endfor %}
+
</table>
{% if g.fas_user %}
<p>
- {% if nextaction == 'confirm' %}
+ {% if nextaction == 'vote' %}
+ <input type="hidden" name="action" value="submit" />
<input type="submit" name="{{nextaction}}" value="Submit" />
{% else %}
+ <input type="hidden" name="action" value="preview" />
<input type="submit" name="{{nextaction}}" value="Preview" />
{% endif %}
</p>
+ <a href="{{ url_for('open_elections') }}">Back</a>
+ {{ form.csrf_token }}
{% endif %}
</form>
commit e54eec82a9935437f3ea03218330aef59a42519e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:30:38 2014 +0200
Port the simple voting logic to use the forms just created
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index b1bb8c2..3cf40e3 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -183,51 +183,38 @@ def vote_simple(election):
SESSION, flask.g.fas_user.username, election.id, count=True)
num_candidates = election.candidates.count()
- next_action = 'vote'
- candidate_id = -1
- if flask.request.method == 'POST':
- next_action = ''
- form_values = flask.request.form.values()
- if 'Submit' in form_values:
- try:
- candidates_id = [cand.id for cand in election.candidates]
- candidate_id = int(flask.request.form['candidate'])
- except ValueError:
- flask.flash("Invalid Ballot!", "error")
- return safe_redirect_back()
- if candidate_id not in candidates_id:
- flask.flash("Invalid vote, this candidate is not listed for "
- "this election", "error")
- return safe_redirect_back()
+ cand_name = {}
+ for candidate in election.candidates:
+ cand_name[candidate.name] = candidate.id
+ next_action = 'confirm'
+
+ form = forms.get_simple_voting_form(
+ candidates=election.candidates,
+ max_range=num_candidates)
+
+ if form.validate_on_submit():
+ if form.action.data == 'submit':
+ for candidate in form:
+ if candidate.short_name in ['csrf_token', 'action']:
+ continue
- new_vote = models.Vote(
- election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=candidate_id,
- value=1
- )
- SESSION.add(new_vote)
+ new_vote = models.Vote(
+ election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=cand_name[candidate.data],
+ value=1,
+ )
+ SESSION.add(new_vote)
SESSION.commit()
flask.flash("Your vote has been recorded. Thank you!")
return safe_redirect_back()
- elif 'Preview' in form_values:
- if ('candidate' in flask.request.form):
- try:
- candidate_id = int(flask.request.form['candidate'])
- flask.flash("Please confirm your vote!")
- except ValueError:
- next_action = 'vote'
- flask.flash("Invalid data")
- else:
- flask.flash("Please vote for a candidate")
- next_action = 'vote'
-
- if (next_action != 'vote'):
- next_action = 'confirm'
+ if form.action.data == 'preview':
+ flask.flash("Please confirm your vote!")
+ next_action = 'vote'
usernamemap = {}
if (election.candidates_are_fasusers): # pragma: no cover
@@ -242,9 +229,9 @@ def vote_simple(election):
return flask.render_template(
'vote_simple.html',
election=election,
+ form=form,
num_candidates=num_candidates,
usernamemap=usernamemap,
- candidate_id=candidate_id,
nextaction=next_action)
commit 63b4de6e8caec95b0c22c3319546b3d995e20c9b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:30:15 2014 +0200
Add forms.get_simple_voting_form that dynamically generate the form for simple voting
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 267783e..3e7f7c9 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -121,3 +121,22 @@ def get_range_voting_form(candidates, max_range):
setattr(RangeVoting, candidate.name, field)
return RangeVoting()
+
+
+def get_simple_voting_form(candidates, max_range):
+ class SimpleVoting(wtf.Form):
+ action = wtforms.HiddenField()
+
+ titles = []
+ for candidate in candidates:
+ title = candidate.name
+ if candidate.url:
+ title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
+ titles.append((title, title))
+ field = wtforms.RadioField(
+ 'Candidates',
+ choices=titles
+ )
+ setattr(SimpleVoting, 'candidates', field)
+
+ return SimpleVoting()
commit ce572530f6806492f37e015580112b7d3c530a7b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:19:07 2014 +0200
Rename get_voting_range_form to get_range_voting_form and VotingRange to RangeVoting
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index ade7fc0..b1bb8c2 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -131,7 +131,7 @@ def vote_range(election):
cand_name[candidate.name] = candidate.id
next_action = 'confirm'
- form = forms.get_voting_range_form(
+ form = forms.get_range_voting_form(
candidates=election.candidates,
max_range=num_candidates)
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index bc738af..267783e 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -106,8 +106,8 @@ class ConfirmationForm(wtf.Form):
pass
-def get_voting_range_form(candidates, max_range):
- class VotingRange(wtf.Form):
+def get_range_voting_form(candidates, max_range):
+ class RangeVoting(wtf.Form):
action = wtforms.HiddenField()
for candidate in candidates:
@@ -118,6 +118,6 @@ def get_voting_range_form(candidates, max_range):
title,
choices=[(str(item), item) for item in range(max_range + 1)]
)
- setattr(VotingRange, candidate.name, field)
+ setattr(RangeVoting, candidate.name, field)
- return VotingRange()
+ return RangeVoting()
commit fafd27f9bab0da5e42ae953f20b44f36686cf412
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:09:16 2014 +0200
Fix the tests for the change in range voting
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index 2864ed0..cbb455b 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -144,7 +144,7 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
# Election in progress
@@ -192,7 +192,7 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
# Invalid candidate id
@@ -200,18 +200,40 @@ class FlaskElectionstests(ModelFlasktests):
'9': 1,
'5': 3,
'6': 2,
- 'name': 'Preview',
+ 'action': 'preview',
}
output = self.app.post('/vote/test_election3', data=data)
- self.assertEqual(output.status_code, 400)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 3)
- # Invalid vote: too low
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ # Invalid candidate id
data = {
- '4': -1,
+ '9': 1,
'5': 3,
'6': 2,
- 'name': 'Preview',
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/vote/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 3)
+
+ # Invalid vote: too low
+ data = {
+ 'Toshio': -1,
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
}
output = self.app.post('/vote/test_election3', data=data)
@@ -219,15 +241,19 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Invalid vote: too high
data = {
- '4': 5,
- '5': 3,
- '6': 2,
- 'name': 'Preview',
+ 'Toshio': 5,
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
}
output = self.app.post('/vote/test_election3', data=data)
@@ -235,15 +261,19 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Invalid vote: Not numeric
data = {
- '4': 'a',
- '5': 3,
- '6': 2,
- 'name': 'Preview',
+ 'Toshio': 'a',
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
}
output = self.app.post('/vote/test_election3', data=data)
@@ -251,15 +281,19 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="vote" value="Preview" />'
+ '<input type="hidden" name="action" value="preview" />'
in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Valid input
data = {
- '4': 1,
- '5': 3,
- '6': 2,
- 'name': 'Preview',
+ 'Toshio': 1,
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'preview',
+ 'csrf_token': csrf_token,
}
output = self.app.post('/vote/test_election3', data=data)
@@ -267,7 +301,7 @@ class FlaskElectionstests(ModelFlasktests):
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
self.assertTrue(
- '<input type="submit" name="confirm" value="Submit" />'
+ '<input type="hidden" name="action" value="submit" />'
in output.data)
self.assertTrue('<div class="vtmedcool">' in output.data)
self.assertTrue('<div class="vthot">' in output.data)
@@ -286,65 +320,78 @@ class FlaskElectionstests(ModelFlasktests):
user = FakeUser(['voters'], username='pingou')
with user_set(fedora_elections.APP, user):
- # Invalid candidate id
+ # No csrf token provided
data = {
'9': 1,
'5': 3,
'6': 2,
- 'name': 'Submit',
+ 'action': 'submit',
}
output = self.app.post('/vote/test_election3', data=data)
- self.assertEqual(output.status_code, 400)
+ self.assertEqual(output.status_code, 200)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 3)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
# Invalid vote: too low
data = {
- '4': -1,
- '5': 3,
- '6': 2,
- 'name': 'Submit',
+ 'Toshio': -1,
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
- '/vote/test_election3', data=data, follow_redirects=True)
+ '/vote/test_election3', data=data)
self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<li class="message">Invalid Ballot!</li' in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Invalid vote: too high
data = {
- '4': 5,
- '5': 3,
- '6': 2,
- 'name': 'Submit',
+ 'Toshio': 5,
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
'/vote/test_election3', data=data, follow_redirects=True)
self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<li class="message">Invalid Ballot!</li' in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Invalid vote: Not numeric
data = {
- '4': 'a',
- '5': 3,
- '6': 2,
- 'name': 'Submit',
+ 'Toshio': 'a',
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
'/vote/test_election3', data=data, follow_redirects=True)
self.assertEqual(output.status_code, 200)
- self.assertTrue(
- '<li class="message">Invalid Ballot!</li' in output.data)
+ self.assertEqual(
+ output.data.count('<td class="error">Not a valid choice</td>'),
+ 1)
# Valid input
data = {
- '4': 1,
- '5': 3,
- '6': 2,
- 'name': 'Submit',
+ 'Toshio': 1,
+ 'Kevin': 3,
+ 'Ralph': 2,
+ 'action': 'submit',
+ 'csrf_token': csrf_token,
}
output = self.app.post(
commit 87312b4757c5ef5b64eae9b2edb6bcc55e0310be
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:08:51 2014 +0200
Allow to vote up to the number of candidates
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index d06097c..bc738af 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -116,7 +116,7 @@ def get_voting_range_form(candidates, max_range):
title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
field = wtforms.SelectField(
title,
- choices=[(str(item), item) for item in range(max_range)]
+ choices=[(str(item), item) for item in range(max_range + 1)]
)
setattr(VotingRange, candidate.name, field)
commit 8cee25f11b3bbcdc0d61311b5a2d34b9d1316f3b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 13:29:14 2014 +0200
Adjust the range voting form to make use of the wtforms form created for it
diff --git a/fedora_elections/templates/vote_range.html b/fedora_elections/templates/vote_range.html
index d88c7a8..5e0ff04 100644
--- a/fedora_elections/templates/vote_range.html
+++ b/fedora_elections/templates/vote_range.html
@@ -1,3 +1,5 @@
+{% from "_formhelpers.html" import render_field_in_row, render_field_data_in_row %}
+
{% extends "base.html" %}
{% block body %}
<h2>{{election.shortdesc}}</h2>
@@ -8,73 +10,27 @@
<form action="{{ url_for('vote', election_alias=election.alias) }}" method="post">
<table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- <a href="{{candidate.url}}">[Info]</a>
- </td>
- <!-- If user is logged in: -->
- {% if g.fas_user %}
- {% if nextaction == 'vote' %}
- <td>
- <select name="{{candidate.id}}">
- {% for opt in range(0, num_candidates+1) %}
- <option value={{opt}}>{{opt}}</option>
- {% endfor %}
- </select>
- </td>
- {% endif %}
- {% if nextaction == 'confirm' %}
- <td>
- {% set cid = candidate.id %}
- <input type="hidden" name="{{candidate.id}}"
- value="{{voteinfo[cid]}}" />
- {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
- {% if weightpercent >= 90 %}
- <div class="vthot">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 70 %}
- <div class="vtwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 50 %}
- <div class="vtmedwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 30 %}
- <div class="vtmedcool">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 10 %}
- <div class="vtcool">
- {{voteinfo[cid]}}
- </div>
- {% else %}
- <div class="vtcold">
- {{voteinfo[cid]}}
- </div>
- {% endif %}
- </td>
- {% endif %}
- {% endif %}
- <!-- End if user logged in -->
- </tr>
+ {% for field in form if field.widget.input_type != 'hidden' %}
+ {% if nextaction == 'vote' %}
+ {{ render_field_data_in_row(field, num_candidates=num_candidates) }}
+ {% else %}
+ {{ render_field_in_row(field) }}
+ {% endif %}
{% endfor %}
+
</table>
{% if g.fas_user %}
<p>
- {% if nextaction == 'confirm' %}
+ {% if nextaction == 'vote' %}
+ <input type="hidden" name="action" value="submit" />
<input type="submit" name="{{nextaction}}" value="Submit" />
{% else %}
+ <input type="hidden" name="action" value="preview" />
<input type="submit" name="{{nextaction}}" value="Preview" />
{% endif %}
</p>
+ <a href="{{ url_for('open_elections') }}">Back</a>
+ {{ form.csrf_token }}
{% endif %}
</form>
commit a6c5e2ea52159501d70f9cc7c53150ab378361c2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 13:28:06 2014 +0200
Update the range voting method to relying on a wtforms form
This adds CSRF protection to the voting pages and takes care of the data
validation directly in the form.
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index e477357..ade7fc0 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -126,35 +126,28 @@ def vote_range(election):
num_candidates = election.candidates.count()
- uvotes = {}
+ cand_name = {}
for candidate in election.candidates:
- uvotes[candidate.id] = ""
- next_action = 'vote'
+ cand_name[candidate.name] = candidate.id
+ next_action = 'confirm'
- if flask.request.method == 'POST':
- next_action = ''
- form_values = flask.request.form.values()
- if 'Submit' in form_values:
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote >= 0 and vote <= num_candidates:
- uvotes[candidate.id] = int(vote)
- else:
- flask.flash("Invalid Ballot!")
- return safe_redirect_back()
+ form = forms.get_voting_range_form(
+ candidates=election.candidates,
+ max_range=num_candidates)
- except ValueError:
- flask.flash("Invalid Ballot!")
- return safe_redirect_back()
+ if form.validate_on_submit():
+
+ if form.action.data == 'submit':
+ for candidate in form:
+ if candidate.short_name in ['csrf_token', 'action']:
+ continue
- for uvote in uvotes:
new_vote = models.Vote(
election_id=election.id,
voter=flask.g.fas_user.username,
timestamp=datetime.now(),
- candidate_id=uvote,
- value=uvotes[uvote]
+ candidate_id=cand_name[candidate.short_name],
+ value=candidate.data,
)
SESSION.add(new_vote)
SESSION.commit()
@@ -162,27 +155,9 @@ def vote_range(election):
flask.flash("Your vote has been recorded. Thank you!")
return safe_redirect_back()
- elif 'Preview' in form_values:
+ if form.action.data == 'preview':
flask.flash("Please confirm your vote!")
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote > num_candidates:
- flask.flash("Invalid data.")
- uvotes[candidate.id] = 0
- next_action = 'vote'
- elif vote >= 0:
- uvotes[candidate.id] = vote
- else:
- flask.flash("Invalid data2.")
- uvotes[candidate.id] = 0
- next_action = 'vote'
- except ValueError:
- next_action = 'vote'
- flask.flash("Invalid data")
-
- if next_action != 'vote':
- next_action = 'confirm'
+ next_action = 'vote'
usernamemap = {}
if (election.candidates_are_fasusers): # pragma: no cover
@@ -197,9 +172,9 @@ def vote_range(election):
return flask.render_template(
'vote_range.html',
election=election,
+ form=form,
num_candidates=num_candidates,
usernamemap=usernamemap,
- voteinfo=uvotes,
nextaction=next_action)
commit 9829ab1c16f7e54f5794c706c82aaac08ac2901f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 13:26:57 2014 +0200
Add macro to render the choices when confirming a vote
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
index e11eb67..e0a59b6 100644
--- a/fedora_elections/templates/_formhelpers.html
+++ b/fedora_elections/templates/_formhelpers.html
@@ -20,3 +20,30 @@
%}<td class="error">{{ error }}</td>{% endfor %}{% endif %}
</tr>
{% endmacro %}
+
+{% macro render_field_data_in_row(field, num_candidates, after="") %}
+<tr>
+ <td>{{ field.label }}</td>
+ <td>
+ {% set weightpercent = (field.data | int *100) / num_candidates %}
+ {% if weightpercent >= 90 %}
+ <div class="vthot">
+ {% elif weightpercent >= 70 %}
+ <div class="vtwarm">
+ {% elif weightpercent >= 50 %}
+ <div class="vtmedwarm">
+ {% elif weightpercent >= 30 %}
+ <div class="vtmedcool">
+ {% elif weightpercent >= 10 %}
+ <div class="vtcool">
+ {% else %}
+ <div class="vtcold">
+ {% endif %}
+ {{ field }}
+ </div>
+ </td>
+{% if after %} <td>{{ after }}</td>{% endif %}
+{% if field.errors %}{% for error in field.errors
+%}<td class="error">{{ error }}</td>{% endfor %}{% endif %}
+</tr>
+{% endmacro %}
commit 4e254d05d65ab2256c05869fb4da3b6c5660b7ff
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 13:26:32 2014 +0200
Add dynamically generated form used for range voting
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index f5f40a9..d06097c 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -104,3 +104,20 @@ class MultiCandidateForm(wtf.Form):
class ConfirmationForm(wtf.Form):
pass
+
+
+def get_voting_range_form(candidates, max_range):
+ class VotingRange(wtf.Form):
+ action = wtforms.HiddenField()
+
+ for candidate in candidates:
+ title = candidate.name
+ if candidate.url:
+ title = '%s <a href="%s">[Info]</a>' % (title, candidate.url)
+ field = wtforms.SelectField(
+ title,
+ choices=[(str(item), item) for item in range(max_range)]
+ )
+ setattr(VotingRange, candidate.name, field)
+
+ return VotingRange()
commit 74d5baf4ce1584da589bdc058cee9c5d2bcbd0c4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu May 29 18:30:17 2014 +0200
Let's be a little more precise/concrete about what the check allows
diff --git a/fedora_elections/templates/results.html b/fedora_elections/templates/results.html
index ec97fe2..c4581b1 100644
--- a/fedora_elections/templates/results.html
+++ b/fedora_elections/templates/results.html
@@ -21,7 +21,12 @@
{% elif loop.index > election.seats_elected and votes > candidate.vote_count and not flag %}
{# if we are above the number of user that will be selected (seats
available), check if the number of votes for this candidate is lower than
- the number of votes for the last candidate and if the Flag is False #}
+ the number of votes for the last candidate and if the Flag is False
+ So this takes care of the case where there are 10 seats elected and the 11th
+ candidate as the same score as the 10th one.
+ In this case we would end up with one more person that the number of seats
+ available and we'll need to either find a way to select one over the other
+ or deal with having one more candidate accepted #}
{% set flag = True %}
{% set lastrow = True %}
{% else %}
commit 3c38052b6ebb09aefcf53b2e27feda8a72d34af8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu May 29 18:25:20 2014 +0200
Comment the logic of the conditions to select where to place the limit
diff --git a/fedora_elections/templates/results.html b/fedora_elections/templates/results.html
index b8815ff..ec97fe2 100644
--- a/fedora_elections/templates/results.html
+++ b/fedora_elections/templates/results.html
@@ -14,12 +14,20 @@
{% for candidate in election.candidates|sort(attribute='vote_count', reverse=True) %}
{% if loop.index <= election.seats_elected %}
+ {# If we are below the number of user that will be selected, get the number
+ of votes and the flag to False#}
{% set flag = False %}
{% set votes = candidate.vote_count %}
{% elif loop.index > election.seats_elected and votes > candidate.vote_count and not flag %}
+ {# if we are above the number of user that will be selected (seats
+ available), check if the number of votes for this candidate is lower than
+ the number of votes for the last candidate and if the Flag is False #}
{% set flag = True %}
{% set lastrow = True %}
{% else %}
+ {# we are above the number of seats available, the number of votes is below
+ that of the last candidate above selected and the Flag is True which means
+ we already passed the condition above #}
{% set lastrow = False %}
{% endif %}
<tr class="{% if lastrow == True %}firstout{% endif %} {{ loop.cycle('row_odd', 'row_even') }}">
commit fd8286e02e53c94898c174533e623a5b12a81052
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 19:00:25 2014 +0200
Show the actual results in the result page
Rework the result table to display a nice red-bar where the limit between
the elected candidates and the non-elected candidates is.
diff --git a/fedora_elections/static/elections.css b/fedora_elections/static/elections.css
index 8aba889..5f67a8a 100644
--- a/fedora_elections/static/elections.css
+++ b/fedora_elections/static/elections.css
@@ -49,6 +49,15 @@ body#admin table {
padding-top:2em;
}
+#results .firstout tr,
+#results .firstout td {
+ border-top: 2px solid red !important;
+}
+
+.row_even, .row_even td {
+ background-color: #F0F0F0;
+}
+
#content {
float:left;
width:74%px;
diff --git a/fedora_elections/templates/results.html b/fedora_elections/templates/results.html
index cc5e849..b8815ff 100644
--- a/fedora_elections/templates/results.html
+++ b/fedora_elections/templates/results.html
@@ -5,13 +5,24 @@
{% if election.url %}
<p><a href="{{ election.url }}">[More Information]</a></p>
{% endif %}
- <table border="1" cellpadding="1">
+ <table id="results">
<tr>
<th>Candidate</th>
<th title="Number of votes received">Votes</th>
</tr>
- {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
- <tr>
+
+
+ {% for candidate in election.candidates|sort(attribute='vote_count', reverse=True) %}
+ {% if loop.index <= election.seats_elected %}
+ {% set flag = False %}
+ {% set votes = candidate.vote_count %}
+ {% elif loop.index > election.seats_elected and votes > candidate.vote_count and not flag %}
+ {% set flag = True %}
+ {% set lastrow = True %}
+ {% else %}
+ {% set lastrow = False %}
+ {% endif %}
+ <tr class="{% if lastrow == True %}firstout{% endif %} {{ loop.cycle('row_odd', 'row_even') }}">
<td>
{% if election.candidates_are_fasusers %}
{{ usernamemap[candidate.id] }}
@@ -31,6 +42,10 @@
<h3>Some statistics about this election</h3>
<table>
<tr>
+ <th>Number of seats elections</th>
+ <td>{{ election.seats_elected }}</td>
+ </tr>
+ <tr>
<th>Number of voters</th>
<td>{{ stats['n_voters'] }}</td>
</tr>
commit e496781f7b9067b9166588f20ea6dd2f8adef7d8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 18:58:32 2014 +0200
Avoid adding empty legal voter groups
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index e70be01..32a4ee6 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -98,11 +98,12 @@ def admin_new_election():
# Add legal voters groups if there are any
for voter_grp in form.lgl_voters.data.split(','):
- lglvoters = models.LegalVoter(
- election=election,
- group_name=voter_grp
- )
- SESSION.add(lglvoters)
+ if voter_grp.strip():
+ lglvoters = models.LegalVoter(
+ election=election,
+ group_name=voter_grp
+ )
+ SESSION.add(lglvoters)
SESSION.commit()
commit 2280f067602b36f25820e691e2c63f568e01e171
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:57:49 2014 +0200
Cover adding admin groups when testing admin_new_election
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index 838b92e..5f0402a 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -269,6 +269,7 @@ class FlaskAdmintests(ModelFlasktests):
'seats_elected': 2,
'candidates_are_fasusers': False,
'embargoed': True,
+ 'admin_grp': 'testers, sysadmin-main,,',
'csrf_token': csrf_token,
}
@@ -282,6 +283,9 @@ class FlaskAdmintests(ModelFlasktests):
'<a href="/admin/new_election/edit">' in output.data)
self.assertTrue(
'<p>There are no candidates.</p>' in output.data)
+ self.assertTrue(
+ '<li>Admin groups: sysadmin-main, testers</li>'
+ in output.data)
def test_admin_edit_election(self):
""" Test the admin_edit_election function. """
commit e521ec9eab264a92dbcdc099d4fcdc809cd12b14
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:55:37 2014 +0200
Split the tests for editing admin groups or legal voters into their own method
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index 6a9b93e..838b92e 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -385,6 +385,23 @@ class FlaskAdmintests(ModelFlasktests):
'<p>3 candidates found</p>'
in output.data)
+ def test_admin_edit_election_admin_groups(self):
+ """ Test the admin_edit_election function when editing admin groups. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/edit')
+ self.assertEqual(output.status_code, 404)
+
+ self.setup_db()
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election2/edit')
+ self.assertEqual(output.status_code, 200)
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
# Edit Admin Group
# Check election before edit
@@ -452,6 +469,22 @@ class FlaskAdmintests(ModelFlasktests):
'<li>Admin groups: sysadmin-main</li>'
in output.data)
+ def test_admin_edit_election_legal_voters(self):
+ """ Test the admin_edit_election function when editing legal voters. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/edit')
+ self.assertEqual(output.status_code, 404)
+
+ self.setup_db()
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election3/edit')
+ self.assertEqual(output.status_code, 200)
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
# Edit LegalVoter Group
# Check election before edit
commit 6885bc78264e502c312016016d7fd3e84d544791
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:50:56 2014 +0200
Add test checking the behavior when the user is not part of the LegalVoters
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index 74b5126..2864ed0 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -99,6 +99,19 @@ class FlaskElectionstests(ModelFlasktests):
'<li class="error">You need to be in one another group than '
'CLA to vote</li>' in output.data)
+ user = FakeUser(['packager'], username='pingou')
+ with user_set(fedora_elections.APP, user):
+
+ output = self.app.get('/vote/test_election3')
+ self.assertEqual(output.status_code, 302)
+
+ # Election closed and results open
+ output = self.app.get(
+ '/vote/test_election3', follow_redirects=True)
+ self.assertTrue(
+ '<li class="error">You are not among the groups that are '
+ 'allowed to vote for this election</li>' in output.data)
+
user = FakeUser(['voters'], username='pingou')
with user_set(fedora_elections.APP, user):
commit 93afafdceaa67a2fca0b69e5d30c96bc3b531a2c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:48:28 2014 +0200
Add test for editing an election with legal voters
These tests are checking how the application behaves when we add or remove
legal voter groups
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index fb408eb..6a9b93e 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -416,9 +416,9 @@ class FlaskAdmintests(ModelFlasktests):
self.assertTrue(
'<li class="message">Election "test_election2" saved</li>'
in output.data)
- # We edited the seats_elected from 1 to 2
self.assertTrue(
'<li>Number elected: 2</li>' in output.data)
+ # We edited the admin groups
self.assertTrue(
'<li>Admin groups: sysadmin-main, testers</li>'
in output.data)
@@ -445,13 +445,77 @@ class FlaskAdmintests(ModelFlasktests):
self.assertTrue(
'<li class="message">Election "test_election2" saved</li>'
in output.data)
- # We edited the seats_elected from 1 to 2
self.assertTrue(
'<li>Number elected: 2</li>' in output.data)
+ # We edited the admin groups
self.assertTrue(
'<li>Admin groups: sysadmin-main</li>'
in output.data)
+ # Edit LegalVoter Group
+
+ # Check election before edit
+ output = self.app.get('/admin/test_election3/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<li>Number elected: 1</li>' in output.data)
+ self.assertTrue('<li>Admin groups: </li>' in output.data)
+ self.assertTrue('<li>Legal voters: voters</li>' in output.data)
+
+ # Add a new admin group: sysadmin-main
+ data = {
+ 'alias': 'test_election3',
+ 'shortdesc': 'test election 3 shortdesc',
+ 'description': 'test election 3 description',
+ 'voting_type': 'range',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY - timedelta(days=2),
+ 'end_date': TODAY + timedelta(days=3),
+ 'seats_elected': '1',
+ 'candidates_are_fasusers': False,
+ 'embargoed': False,
+ 'lgl_voters': 'voters, sysadmin-main',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election3/edit', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Election "test_election3" saved</li>'
+ in output.data)
+ # We edited the legal_voters
+ self.assertTrue(
+ '<li>Legal voters: sysadmin-main, voters</li>'
+ in output.data)
+
+ # Remove an existing group: voters
+ data = {
+ 'alias': 'test_election3',
+ 'shortdesc': 'test election 3 shortdesc',
+ 'description': 'test election 3 description',
+ 'voting_type': 'range',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY - timedelta(days=2),
+ 'end_date': TODAY + timedelta(days=3),
+ 'seats_elected': '1',
+ 'candidates_are_fasusers': False,
+ 'embargoed': False,
+ 'lgl_voters': 'sysadmin-main',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election3/edit', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Election "test_election3" saved</li>'
+ in output.data)
+ # We edited the legal_voters
+ self.assertTrue(
+ '<li>Legal voters: sysadmin-main</li>'
+ in output.data)
+
def test_admin_add_candidate(self):
""" Test the admin_add_candidate function. """
user = FakeUser(
commit 701a20cf1c1ab14a4db8b1202736a52cc5f70dc8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:48:04 2014 +0200
Adjust the tests so that the FakeFasUser is part of the group allowed to vote
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index 3cd26e0..74b5126 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -99,7 +99,7 @@ class FlaskElectionstests(ModelFlasktests):
'<li class="error">You need to be in one another group than '
'CLA to vote</li>' in output.data)
- user = FakeUser(['packager'], username='pingou')
+ user = FakeUser(['voters'], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get('/vote/test_election')
@@ -142,7 +142,7 @@ class FlaskElectionstests(ModelFlasktests):
'<input type="submit" name="vote" value="Preview" />'
in output.data)
- user = FakeUser(['packager'], username='toshio')
+ user = FakeUser(['voters'], username='toshio')
with user_set(fedora_elections.APP, user):
# Election in progress
@@ -164,7 +164,7 @@ class FlaskElectionstests(ModelFlasktests):
self.setup_db()
- user = FakeUser(['packager'], username='toshio')
+ user = FakeUser(['voters'], username='toshio')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/vote/test_election3', follow_redirects=True)
@@ -172,7 +172,7 @@ class FlaskElectionstests(ModelFlasktests):
'class="message">You have already voted in the election!</'
in output.data)
- user = FakeUser(['packager'], username='pingou')
+ user = FakeUser(['voters'], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/vote/test_election3')
@@ -271,7 +271,7 @@ class FlaskElectionstests(ModelFlasktests):
self.setup_db()
- user = FakeUser(['packager'], username='pingou')
+ user = FakeUser(['voters'], username='pingou')
with user_set(fedora_elections.APP, user):
# Invalid candidate id
data = {
commit 4064980cf19599c1571d226917e5c6b01bee7370
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:47:48 2014 +0200
Specify a group of user allowed to vote on election #3
diff --git a/tests/test_election.py b/tests/test_election.py
index 4a8ed5b..342c253 100644
--- a/tests/test_election.py
+++ b/tests/test_election.py
@@ -108,6 +108,15 @@ class Electiontests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ # Add a LegalVoter group to election #3
+ voters = models.LegalVoter(
+ election=obj,
+ group_name='voters',
+ )
+ self.session.add(voters)
+ self.session.commit()
+ self.assertNotEqual(obj, admin)
+
# Election created but not open
obj = models.Election( # id:4
shortdesc='test election 4 shortdesc',
commit 61e6b277e7e95aa385e43cf6fcb74bc17771be29
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:37:20 2014 +0200
Add test checking the error message if the user has 0 groups
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index bdd73eb..3cd26e0 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -87,6 +87,18 @@ class FlaskElectionstests(ModelFlasktests):
'<li class="error">You must sign the CLA to vote</li>'
in output.data)
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+
+ output = self.app.get('/vote/test_election')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get(
+ '/vote/test_election', follow_redirects=True)
+ self.assertTrue(
+ '<li class="error">You need to be in one another group than '
+ 'CLA to vote</li>' in output.data)
+
user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
commit 10d59f2da65aac5cede2e83f8a5303d0bda7676e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:36:39 2014 +0200
Fix the unit-tests so that the FakeFasUser used has at least 1 group
And fix checking the error message with cla_done = False
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index e808963..bdd73eb 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -55,7 +55,7 @@ class FlaskElectionstests(ModelFlasktests):
'<title>OpenID transaction in progress</title>' in output.data
or 'discoveryfailure' in output.data)
- user = FakeUser([], username='pingou')
+ user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get('/vote/test_election')
self.assertEqual(output.status_code, 302)
@@ -84,10 +84,10 @@ class FlaskElectionstests(ModelFlasktests):
output = self.app.get(
'/vote/test_election', follow_redirects=True)
self.assertTrue(
- '<li class="error">You need to be in one another group than '
- 'CLA to vote</li>' in output.data)
+ '<li class="error">You must sign the CLA to vote</li>'
+ in output.data)
- user = FakeUser([], username='pingou')
+ user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get('/vote/test_election')
@@ -130,7 +130,7 @@ class FlaskElectionstests(ModelFlasktests):
'<input type="submit" name="vote" value="Preview" />'
in output.data)
- user = FakeUser([], username='toshio')
+ user = FakeUser(['packager'], username='toshio')
with user_set(fedora_elections.APP, user):
# Election in progress
@@ -152,7 +152,7 @@ class FlaskElectionstests(ModelFlasktests):
self.setup_db()
- user = FakeUser([], username='toshio')
+ user = FakeUser(['packager'], username='toshio')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/vote/test_election3', follow_redirects=True)
@@ -160,7 +160,7 @@ class FlaskElectionstests(ModelFlasktests):
'class="message">You have already voted in the election!</'
in output.data)
- user = FakeUser([], username='pingou')
+ user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/vote/test_election3')
@@ -259,7 +259,7 @@ class FlaskElectionstests(ModelFlasktests):
self.setup_db()
- user = FakeUser([], username='pingou')
+ user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
# Invalid candidate id
data = {
@@ -343,7 +343,7 @@ class FlaskElectionstests(ModelFlasktests):
self.setup_db()
- user = FakeUser([], username='toshio')
+ user = FakeUser(['packager'], username='toshio')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/vote/test_election5', follow_redirects=True)
@@ -351,7 +351,7 @@ class FlaskElectionstests(ModelFlasktests):
'class="message">You have already voted in the election!</'
in output.data)
- user = FakeUser([], username='pingou')
+ user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/vote/test_election5')
@@ -419,7 +419,7 @@ class FlaskElectionstests(ModelFlasktests):
self.setup_db()
- user = FakeUser([], username='pingou')
+ user = FakeUser(['packager'], username='pingou')
with user_set(fedora_elections.APP, user):
# Invalid candidate id
data = {
@@ -508,7 +508,7 @@ class FlaskElectionstests(ModelFlasktests):
'pending formal announcement.</li>' in output.data)
self.assertTrue('<h3>Current elections</h3>' in output.data)
- user = FakeUser([], username='toshio')
+ user = FakeUser(['packager'], username='toshio')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/results/test_election2', follow_redirects=True)
@@ -541,9 +541,7 @@ class FlaskElectionstests(ModelFlasktests):
'<h3>Some statistics about this election</h3>'
in output.data)
- user = FakeUser(
- ['gitr2spec'],
- username='kevin')
+ user = FakeUser(['gitr2spec'], username='kevin')
with user_set(fedora_elections.APP, user):
output = self.app.get(
'/results/test_election3', follow_redirects=True)
commit cb9204232af452a1e4ee315862f93e3cc7429eaf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:34:49 2014 +0200
Fix the @login_required to check to cla_done and groups > 0
This commit fixes @login_required that was checking cla_done and returning
an error message about CLA+1.
We now check cla_done and groups > 0 and report an appropriate error message
in both cases.
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index b8b7885..e477357 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -46,6 +46,10 @@ def login_required(f):
'auth_login', next=flask.request.url))
elif not flask.g.fas_user.cla_done:
flask.flash(
+ 'You must sign the CLA to vote', 'error')
+ return safe_redirect_back()
+ elif len(flask.g.fas_user.groups) == 0:
+ flask.flash(
'You need to be in one another group than CLA to vote',
'error')
return safe_redirect_back()
commit 598338d52a50a52052d3a68a3d735cfcd6a0ec1d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:30:52 2014 +0200
Add test for the redirect from the voting page is the user did not sign the CLA
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index 6506a08..e808963 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -75,6 +75,18 @@ class FlaskElectionstests(ModelFlasktests):
self.setup_db()
+ user = FakeUser([], username='pingou', cla_done=False)
+ with user_set(fedora_elections.APP, user):
+
+ output = self.app.get('/vote/test_election')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get(
+ '/vote/test_election', follow_redirects=True)
+ self.assertTrue(
+ '<li class="error">You need to be in one another group than '
+ 'CLA to vote</li>' in output.data)
+
user = FakeUser([], username='pingou')
with user_set(fedora_elections.APP, user):
commit 3c721970d437a1f5501e27a3d6d56cb0ae71bdde
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:12:20 2014 +0200
If there are legal voters and the user is not one of them, redirect him/her back
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index d82bae7..b8b7885 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -91,6 +91,14 @@ def vote(election_alias):
if not isinstance(election, models.Election):
return election
+ if election.legal_voters_list:
+ if len(set(flask.g.fas_user.groups).intersection(
+ set(election.legal_voters_list))) == 0:
+ flask.flash(
+ 'You are not among the groups that are allowed to vote for this '
+ 'election', 'error')
+ return safe_redirect_back()
+
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
commit f51906fa56568e9d749c25e96a82646546c8491b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:10:59 2014 +0200
Use safe_redirect_back instead of forcing the return to index
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 92f64de..d82bae7 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -48,7 +48,7 @@ def login_required(f):
flask.flash(
'You need to be in one another group than CLA to vote',
'error')
- return flask.redirect(flask.url_for('index'))
+ return safe_redirect_back()
return f(*args, **kwargs)
return decorated_function
commit c92438f6ca22301152dfa327c6d2f04479938de7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:08:03 2014 +0200
Voting to an election requires CLA+1 at minimum
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index f14a900..92f64de 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -44,6 +44,12 @@ def login_required(f):
if not is_authenticated():
return flask.redirect(flask.url_for(
'auth_login', next=flask.request.url))
+ elif not flask.g.fas_user.cla_done:
+ flask.flash(
+ 'You need to be in one another group than CLA to vote',
+ 'error')
+ return flask.redirect(flask.url_for('index'))
+
return f(*args, **kwargs)
return decorated_function
commit 6e57cd622cba3ea37d8fac846a452a91c7cc502e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:57:02 2014 +0200
Implement legal_voters adding/editing/deleting
This allow the admin to restrict the election to one or more FAS groups
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index 6d568c9..e70be01 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -87,12 +87,22 @@ def admin_new_election():
time(23, 59, 59))
SESSION.add(election)
+ # Add admin groups if there are any
for admin_grp in form.admin_grp.data.split(','):
- admin = models.ElectionAdminGroup(
+ if admin_grp.strip():
+ admin = models.ElectionAdminGroup(
+ election=election,
+ group_name=admin_grp.strip(),
+ )
+ SESSION.add(admin)
+
+ # Add legal voters groups if there are any
+ for voter_grp in form.lgl_voters.data.split(','):
+ lglvoters = models.LegalVoter(
election=election,
- group_name=admin_grp.strip(),
+ group_name=voter_grp
)
- SESSION.add(admin)
+ SESSION.add(lglvoters)
SESSION.commit()
@@ -145,7 +155,7 @@ def admin_edit_election(election_alias):
new_groups = set(
[grp.strip() for grp in form.admin_grp.data.split(',')])
- # Add the new groups
+ # Add the new admin groups
for admin_grp in new_groups.difference(admin_groups):
admin = models.ElectionAdminGroup(
election=election,
@@ -153,12 +163,32 @@ def admin_edit_election(election_alias):
)
SESSION.add(admin)
- # Remove the groups that were removed with this edition
+ # Remove the admin groups that were removed with this edition
for admin_grp in admin_groups.difference(new_groups):
admingrp = models.ElectionAdminGroup.by_election_id_and_name(
SESSION, election.id, admin_grp)
SESSION.delete(admingrp)
+ legal_voters = set(election.legal_voters_list)
+
+ new_lgl_voters_groups = set(
+ [grp.strip() for grp in form.lgl_voters.data.split(',')
+ if grp.strip()])
+
+ # Add the new legal voter groups
+ for lgl_grp in new_lgl_voters_groups.difference(legal_voters):
+ admin = models.LegalVoter(
+ election=election,
+ group_name=lgl_grp,
+ )
+ SESSION.add(admin)
+
+ # Remove the legal voter groups that were removed with this edition
+ for lgl_grp in legal_voters.difference(new_lgl_voters_groups):
+ admingrp = models.LegalVoter.by_election_id_and_name(
+ SESSION, election.id, lgl_grp)
+ SESSION.delete(admingrp)
+
SESSION.commit()
fedmsgshim.publish(
topic="election.edit",
@@ -172,6 +202,7 @@ def admin_edit_election(election_alias):
'admin_view_election', election_alias=election.alias))
form.admin_grp.data = ', '.join(election.admin_groups_list)
+ form.lgl_voters.data = ', '.join(election.legal_voters_list)
return flask.render_template(
'admin/election_form.html',
form=form,
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index c066f2d..f5f40a9 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -59,6 +59,9 @@ class ElectionForm(wtf.Form):
embargoed = wtforms.BooleanField('Embargo results?', default=True)
+ lgl_voters = wtforms.TextField(
+ 'Legal voters groups', [wtforms.validators.optional()])
+
admin_grp = wtforms.TextField(
'Admin groups', [wtforms.validators.optional()])
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index ff637e8..e125545 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -116,6 +116,10 @@ class Election(BASE):
return sorted([grp.group_name for grp in self.admin_groups])
@property
+ def legal_voters_list(self):
+ return sorted([grp.group_name for grp in self.legal_voters])
+
+ @property
def status(self):
now = datetime.utcnow()
if now.date() < self.start_date.date():
@@ -298,6 +302,19 @@ class LegalVoter(BASE):
# special names:
# "cla + one" = cla_done + 1 non-cla group
+ @classmethod
+ def by_election_id_and_name(cls, session, election_id, group_name):
+ """ Return the ElectionAdminGroup having a specific election_id
+ and group_name.
+ """
+ return session.query(
+ cls
+ ).filter(
+ cls.election_id == election_id
+ ).filter(
+ cls.group_name == group_name
+ ).first()
+
class Vote(BASE):
__tablename__ = 'votes'
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index bd0fa6d..050c94d 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -21,7 +21,8 @@
{{ render_field_in_row(form.seats_elected) }}
{{ render_field_in_row(form.candidates_are_fasusers) }}
{{ render_field_in_row(form.embargoed) }}
- {{ render_field_in_row(form.admin_grp, after="FAS group allowed to view the result despite the embargo") }}
+ {{ render_field_in_row(form.lgl_voters, after="FAS groups allowed to vote on this election") }}
+ {{ render_field_in_row(form.admin_grp, after="FAS groups allowed to view the result despite the embargo") }}
</table>
</fieldset>
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index 8726599..90ab10b 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -16,6 +16,7 @@
<li>Number elected: {{ election.seats_elected }}</li>
<li>Election type: {{ election.voting_type }}</li>
<li>Admin groups: {{ election.admin_groups_list | join(', ') }}</li>
+ <li>Legal voters: {{ election.legal_voters_list | join(', ') }}</li>
</ul>
<h3>Candidates</h3>
{% if election.candidates.all() %}
commit 4c5033252196111730187cd0496c4f80b7899c71
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:25:45 2014 +0200
Simplify vote_range and vote_simple since they are no longer flask endpoints
- We can directly give them the proper election object, no need to query
it twice
- We can skips the tests we do earlier (avoids doing them twice)
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 23d94ee..f14a900 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -93,31 +93,19 @@ def vote(election_alias):
return safe_redirect_back()
if election.voting_type == 'range':
- return vote_range(election_alias)
+ return vote_range(election)
elif election.voting_type == 'simple':
- return vote_simple(election_alias)
+ return vote_simple(election)
else: # pragma: no cover
flask.flash(
'Unknown election voting type: %s' % election.voting_type)
return safe_redirect_back()
-def vote_range(election_alias):
- election = get_valid_election(election_alias)
-
- if not isinstance(election, models.Election): # pragma: no cover
- return election
-
- if (election.voting_type == 'simple'):
- return flask.redirect(flask.url_for(
- 'vote_simple', election_alias=election_alias))
+def vote_range(election):
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
- if votes > 0:
- flask.flash('You have already voted in the election!')
- return safe_redirect_back()
-
num_candidates = election.candidates.count()
uvotes = {}
@@ -197,23 +185,10 @@ def vote_range(election_alias):
nextaction=next_action)
-def vote_simple(election_alias):
- election = get_valid_election(election_alias)
-
- if not isinstance(election, models.Election): # pragma: no cover
- return election
-
- if (election.voting_type == 'range'):
- return flask.redirect(flask.url_for(
- 'vote_range', election_alias=election_alias))
-
+def vote_simple(election):
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
- if (votes != 0):
- flask.flash('You have already voted in the election!')
- return safe_redirect_back()
-
num_candidates = election.candidates.count()
next_action = 'vote'
candidate_id = -1
commit 5781e3534e267f6d23d8e81ea587d4e96354905a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:22:04 2014 +0200
Adjust unit-tests to the unique URL for voting
- Replace vote_range and vote_simple by vote
- Do not test for redirect as it will always return the proper view
diff --git a/tests/test_flask.py b/tests/test_flask.py
index d18e633..1ff7efe 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -80,7 +80,7 @@ class Flasktests(ModelFlasktests):
self.assertTrue('<h3>Current elections</h3>' in output.data)
self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
- self.assertTrue('<a href="/vote_range/' in output.data)
+ self.assertTrue('<a href="/vote/' in output.data)
self.assertTrue(
'<span class="text">logged in as </span>' in output.data)
self.assertTrue('Vote now!' in output.data)
@@ -282,7 +282,7 @@ class Flasktests(ModelFlasktests):
self.assertTrue('<title>Fedora elections</title>' in output.data)
self.assertTrue('<h3>Next 2 elections</h3>' in output.data)
self.assertTrue('<td>test election 3 shortdesc</td>' in output.data)
- self.assertTrue('<a href="/vote_range/' in output.data)
+ self.assertTrue('<a href="/vote/' in output.data)
self.assertTrue('<a href="/login">login</a>' in output.data)
user = FakeUser([], username='pingou')
@@ -291,7 +291,7 @@ class Flasktests(ModelFlasktests):
self.assertEqual(output.status_code, 200)
self.assertTrue('<title>Fedora elections</title>' in output.data)
self.assertTrue('<h3>Next 2 elections</h3>' in output.data)
- self.assertTrue('<a href="/vote_range/' in output.data)
+ self.assertTrue('<a href="/vote/' in output.data)
self.assertTrue(
'<span class="text">logged in as </span>' in output.data)
self.assertTrue('Vote now!' in output.data)
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index ae72b46..6506a08 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -132,7 +132,7 @@ class FlaskElectionstests(ModelFlasktests):
def test_vote_range(self):
""" Test the vote_range function - the preview part. """
output = self.app.get(
- '/vote_range/test_election', follow_redirects=True)
+ '/vote/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<title>OpenID transaction in progress</title>' in output.data
@@ -143,7 +143,7 @@ class FlaskElectionstests(ModelFlasktests):
user = FakeUser([], username='toshio')
with user_set(fedora_elections.APP, user):
output = self.app.get(
- '/vote_range/test_election3', follow_redirects=True)
+ '/vote/test_election3', follow_redirects=True)
self.assertTrue(
'class="message">You have already voted in the election!</'
in output.data)
@@ -151,13 +151,7 @@ class FlaskElectionstests(ModelFlasktests):
user = FakeUser([], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get(
- '/vote_range/test_election5')
- self.assertTrue(
- 'be redirected automatically to target URL: '
- '<a href="/vote_simple/test_election5">' in output.data)
-
- output = self.app.get(
- '/vote_range/test_election3')
+ '/vote/test_election3')
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
self.assertTrue(
@@ -172,7 +166,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_range/test_election3', data=data)
+ output = self.app.post('/vote/test_election3', data=data)
self.assertEqual(output.status_code, 400)
# Invalid vote: too low
@@ -183,7 +177,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_range/test_election3', data=data)
+ output = self.app.post('/vote/test_election3', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
@@ -199,7 +193,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_range/test_election3', data=data)
+ output = self.app.post('/vote/test_election3', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
@@ -215,7 +209,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_range/test_election3', data=data)
+ output = self.app.post('/vote/test_election3', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
@@ -231,7 +225,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_range/test_election3', data=data)
+ output = self.app.post('/vote/test_election3', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<h2>test election 3 shortdesc</h2>' in output.data)
@@ -245,7 +239,7 @@ class FlaskElectionstests(ModelFlasktests):
def test_vote_range_process(self):
""" Test the vote_range function - the voting part. """
output = self.app.get(
- '/vote_range/test_election', follow_redirects=True)
+ '/vote/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<title>OpenID transaction in progress</title>' in output.data
@@ -263,7 +257,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Submit',
}
- output = self.app.post('/vote_range/test_election3', data=data)
+ output = self.app.post('/vote/test_election3', data=data)
self.assertEqual(output.status_code, 400)
# Invalid vote: too low
@@ -275,7 +269,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_range/test_election3', data=data, follow_redirects=True)
+ '/vote/test_election3', data=data, follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<li class="message">Invalid Ballot!</li' in output.data)
@@ -289,7 +283,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_range/test_election3', data=data, follow_redirects=True)
+ '/vote/test_election3', data=data, follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<li class="message">Invalid Ballot!</li' in output.data)
@@ -303,7 +297,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_range/test_election3', data=data, follow_redirects=True)
+ '/vote/test_election3', data=data, follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<li class="message">Invalid Ballot!</li' in output.data)
@@ -317,7 +311,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_range/test_election3', data=data, follow_redirects=True)
+ '/vote/test_election3', data=data, follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'class="message">Your vote has been recorded. Thank you!</'
@@ -329,7 +323,7 @@ class FlaskElectionstests(ModelFlasktests):
def test_vote_simple(self):
""" Test the vote_simple function - the preview part. """
output = self.app.get(
- '/vote_simple/test_election', follow_redirects=True)
+ '/vote/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<title>OpenID transaction in progress</title>' in output.data
@@ -340,20 +334,15 @@ class FlaskElectionstests(ModelFlasktests):
user = FakeUser([], username='toshio')
with user_set(fedora_elections.APP, user):
output = self.app.get(
- '/vote_simple/test_election5', follow_redirects=True)
+ '/vote/test_election5', follow_redirects=True)
self.assertTrue(
'class="message">You have already voted in the election!</'
in output.data)
user = FakeUser([], username='pingou')
with user_set(fedora_elections.APP, user):
- output = self.app.get('/vote_simple/test_election3')
- self.assertTrue(
- 'be redirected automatically to target URL: '
- '<a href="/vote_range/test_election3">' in output.data)
-
output = self.app.get(
- '/vote_simple/test_election5')
+ '/vote/test_election5')
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
self.assertTrue(
@@ -365,7 +354,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_simple/test_election5', data=data)
+ output = self.app.post('/vote/test_election5', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
@@ -382,7 +371,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_simple/test_election5', data=data)
+ output = self.app.post('/vote/test_election5', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
@@ -396,7 +385,7 @@ class FlaskElectionstests(ModelFlasktests):
'name': 'Preview',
}
- output = self.app.post('/vote_simple/test_election5', data=data)
+ output = self.app.post('/vote/test_election5', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<h2>test election 5 shortdesc</h2>' in output.data)
@@ -410,7 +399,7 @@ class FlaskElectionstests(ModelFlasktests):
def test_vote_simple_process(self):
""" Test the vote_simple function - the voting part. """
output = self.app.get(
- '/vote_simple/test_election', follow_redirects=True)
+ '/vote/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<title>OpenID transaction in progress</title>' in output.data
@@ -427,7 +416,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data,
+ '/vote/test_election5', data=data,
follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
@@ -441,7 +430,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data,
+ '/vote/test_election5', data=data,
follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
@@ -455,7 +444,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data,
+ '/vote/test_election5', data=data,
follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
@@ -468,7 +457,7 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data,
+ '/vote/test_election5', data=data,
follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
commit f01af3bfe463072d2448c9e0a708e8a1dd063388
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:21:40 2014 +0200
Adjust the action url in the vote templates now that there is a unique URL for voting
diff --git a/fedora_elections/templates/vote_range.html b/fedora_elections/templates/vote_range.html
index 71a7ee1..d88c7a8 100644
--- a/fedora_elections/templates/vote_range.html
+++ b/fedora_elections/templates/vote_range.html
@@ -6,7 +6,7 @@
<p><a href="{{election.url}}">[More Information]</a></p>
-<form action="" method="post">
+<form action="{{ url_for('vote', election_alias=election.alias) }}" method="post">
<table border="1" cellpadding="1">
{% for candidate in election.candidates %}
<tr>
diff --git a/fedora_elections/templates/vote_simple.html b/fedora_elections/templates/vote_simple.html
index 0d43183..77dff56 100644
--- a/fedora_elections/templates/vote_simple.html
+++ b/fedora_elections/templates/vote_simple.html
@@ -6,7 +6,7 @@
<p><a href="{{election.url}}">[More Information]</a></p>
-<form action="" method="post">
+<form action="{{ url_for('vote', election_alias=election.alias) }}" method="post">
<table border="0" cellpadding="1">
{% for candidate in election.candidates|sort %}
<tr>
commit 1809a2b157c67f5302a10f944340da31845a314d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:03:07 2014 +0200
No need to have different URL for the different voting type, one is enough
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 8f1b1a3..23d94ee 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -101,9 +101,6 @@ def vote(election_alias):
'Unknown election voting type: %s' % election.voting_type)
return safe_redirect_back()
-
-(a)APP.route('/vote_range/<election_alias>', methods=['GET', 'POST'])
-@login_required
def vote_range(election_alias):
election = get_valid_election(election_alias)
@@ -200,8 +197,6 @@ def vote_range(election_alias):
nextaction=next_action)
-(a)APP.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
-@login_required
def vote_simple(election_alias):
election = get_valid_election(election_alias)
diff --git a/fedora_elections/templates/index.html b/fedora_elections/templates/index.html
index 8f70756..45518d4 100644
--- a/fedora_elections/templates/index.html
+++ b/fedora_elections/templates/index.html
@@ -23,11 +23,7 @@
<td>
{% if election.status == "In progress" %}
{% if election not in voted %}
- {% if election.voting_type == 'range' %}
- <a href="{{ url_for('vote_range', election_alias=election.alias) }}">
- {% elif election.voting_type == 'simple' %}
- <a href="{{ url_for('vote_simple', election_alias=election.alias) }}">
- {% endif %}
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">
Vote now!
</a>
{% endif %}
commit 3d7e19d7cdc085f128ca02ecc14d6744515c9b59
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:36:36 2014 +0200
Explain a little what the admin groups are
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index dd42f1c..bd0fa6d 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -21,7 +21,7 @@
{{ render_field_in_row(form.seats_elected) }}
{{ render_field_in_row(form.candidates_are_fasusers) }}
{{ render_field_in_row(form.embargoed) }}
- {{ render_field_in_row(form.admin_grp) }}
+ {{ render_field_in_row(form.admin_grp, after="FAS group allowed to view the result despite the embargo") }}
</table>
</fieldset>
commit 915284ab8d9a2adf202be45c917e416282dcc7c8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:29:35 2014 +0200
pep8 fixes
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index c766962..ff637e8 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -245,8 +245,6 @@ class ElectionAdminGroup(BASE):
).first()
-
-
class Candidate(BASE):
__tablename__ = 'candidates'
diff --git a/tests/test_election.py b/tests/test_election.py
index 313a98e..4a8ed5b 100644
--- a/tests/test_election.py
+++ b/tests/test_election.py
@@ -83,9 +83,9 @@ class Electiontests(Modeltests):
# Add an AdminGroup to election #2
admin = models.ElectionAdminGroup(
- election=obj,
- group_name='testers',
- )
+ election=obj,
+ group_name='testers',
+ )
self.session.add(admin)
self.session.commit()
self.assertNotEqual(obj, admin)
commit 35c46df93fb5060d72dddcaaa7e52b0c4d392b30
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:28:52 2014 +0200
Add unit-tests for the is_safe_url method
diff --git a/tests/test_flask.py b/tests/test_flask.py
index a70ae60..d18e633 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -154,6 +154,15 @@ class Flasktests(ModelFlasktests):
flask.g.fas_user, 2)
)
+ def test_is_safe_url(self):
+ """ Test the is_safe_url function. """
+ app = flask.Flask('fedora_elections')
+ with app.test_request_context():
+ self.assertFalse(
+ fedora_elections.is_safe_url('https://google.fr'))
+ self.assertTrue(
+ fedora_elections.is_safe_url('/admin/'))
+
def test_auth_login(self):
""" Test the auth_login function. """
app = flask.Flask('fedora_elections')
commit af87afe9652ce270e0574e9aabae0c03568117dd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:22:06 2014 +0200
Add unit-test testing the addition and removal of admin groups when editing an election
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index e770e19..fb408eb 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -385,6 +385,73 @@ class FlaskAdmintests(ModelFlasktests):
'<p>3 candidates found</p>'
in output.data)
+ # Edit Admin Group
+
+ # Check election before edit
+ output = self.app.get('/admin/test_election2/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<li>Number elected: 1</li>' in output.data)
+ self.assertTrue('<li>Admin groups: testers</li>' in output.data)
+
+ # Add a new admin group: sysadmin-main
+ data = {
+ 'alias': 'test_election2',
+ 'shortdesc': 'test election 2 shortdesc',
+ 'description': 'test election 2 description',
+ 'voting_type': 'range',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY - timedelta(days=7),
+ 'end_date': TODAY - timedelta(days=5),
+ 'seats_elected': '2',
+ 'candidates_are_fasusers': False,
+ 'embargoed': False,
+ 'admin_grp': 'testers, sysadmin-main',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election2/edit', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Election "test_election2" saved</li>'
+ in output.data)
+ # We edited the seats_elected from 1 to 2
+ self.assertTrue(
+ '<li>Number elected: 2</li>' in output.data)
+ self.assertTrue(
+ '<li>Admin groups: sysadmin-main, testers</li>'
+ in output.data)
+
+ # Remove an existing group: testers
+ data = {
+ 'alias': 'test_election2',
+ 'shortdesc': 'test election 2 shortdesc',
+ 'description': 'test election 2 description',
+ 'voting_type': 'range',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY - timedelta(days=7),
+ 'end_date': TODAY - timedelta(days=5),
+ 'seats_elected': '2',
+ 'candidates_are_fasusers': False,
+ 'embargoed': False,
+ 'admin_grp': 'sysadmin-main',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election2/edit', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Election "test_election2" saved</li>'
+ in output.data)
+ # We edited the seats_elected from 1 to 2
+ self.assertTrue(
+ '<li>Number elected: 2</li>' in output.data)
+ self.assertTrue(
+ '<li>Admin groups: sysadmin-main</li>'
+ in output.data)
+
def test_admin_add_candidate(self):
""" Test the admin_add_candidate function. """
user = FakeUser(
commit e7b40a47bd3c718541babb2c83ffdaea1b66bfee
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:21:56 2014 +0200
Sort the election's admin groups
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 6c31fe1..c766962 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -113,7 +113,7 @@ class Election(BASE):
@property
def admin_groups_list(self):
- return [grp.group_name for grp in self.admin_groups]
+ return sorted([grp.group_name for grp in self.admin_groups])
@property
def status(self):
commit 8a904fb47ece606f25ed8592f54feafd07fad754
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:16:28 2014 +0200
Test the is_election_admin when the user is and is not in an AdminGroup of that election
diff --git a/tests/test_flask.py b/tests/test_flask.py
index 44bdde7..a70ae60 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -138,6 +138,22 @@ class Flasktests(ModelFlasktests):
self.assertTrue(fedora_elections.is_election_admin(
flask.g.fas_user, 1))
+ self.setup_db()
+
+ with app.test_request_context():
+ flask.g.fas_user = FakeUser('testers')
+ # This is user is not an admin for election #1
+ self.assertFalse(
+ fedora_elections.is_election_admin(
+ flask.g.fas_user, 1)
+ )
+
+ # This is user is an admin for election #2
+ self.assertTrue(
+ fedora_elections.is_election_admin(
+ flask.g.fas_user, 2)
+ )
+
def test_auth_login(self):
""" Test the auth_login function. """
app = flask.Flask('fedora_elections')
commit 6298e3faea18438b30862c5d8656b60f42232ef7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:16:15 2014 +0200
Specify an admin group for the election #2
diff --git a/tests/test_election.py b/tests/test_election.py
index 6b88424..313a98e 100644
--- a/tests/test_election.py
+++ b/tests/test_election.py
@@ -81,6 +81,15 @@ class Electiontests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ # Add an AdminGroup to election #2
+ admin = models.ElectionAdminGroup(
+ election=obj,
+ group_name='testers',
+ )
+ self.session.add(admin)
+ self.session.commit()
+ self.assertNotEqual(obj, admin)
+
# Election open
obj = models.Election( # id:3
shortdesc='test election 3 shortdesc',
commit a02fba9a16b54c1c66ebc1187e18510343e6466c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:10:12 2014 +0200
Enhance coverage of the is_admin function
diff --git a/tests/test_flask.py b/tests/test_flask.py
index 544d01d..44bdde7 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -112,6 +112,12 @@ class Flasktests(ModelFlasktests):
fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'])
self.assertTrue(fedora_elections.is_admin(flask.g.fas_user))
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] = [
+ 'sysadmin-main', 'sysadmin-elections']
+ flask.g.fas_user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'])
+ self.assertTrue(fedora_elections.is_admin(flask.g.fas_user))
+
def test_is_election_admin(self):
""" Test the is_election_admin function. """
app = flask.Flask('fedora_elections')
commit 2903a283a7bf7930848c7063aaf38b030fe65a3a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:08:22 2014 +0200
Elections admins are now admins of every elections
diff --git a/tests/test_flask.py b/tests/test_flask.py
index 761ffa2..544d01d 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -129,7 +129,7 @@ class Flasktests(ModelFlasktests):
)
flask.g.fas_user = FakeUser(
fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'])
- self.assertFalse(fedora_elections.is_election_admin(
+ self.assertTrue(fedora_elections.is_election_admin(
flask.g.fas_user, 1))
def test_auth_login(self):
commit 29b4afe215e76601727fdb0adc4261d13a34e49c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:08:07 2014 +0200
Make the tests pass offline
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index 7503230..e770e19 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -52,7 +52,8 @@ class FlaskAdmintests(ModelFlasktests):
output = self.app.get('/admin/', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data)
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
user = FakeUser([], username='pingou')
with user_set(fedora_elections.APP, user):
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index b1c9fe4..ae72b46 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -52,7 +52,8 @@ class FlaskElectionstests(ModelFlasktests):
output = self.app.get('/vote/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data)
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
user = FakeUser([], username='pingou')
with user_set(fedora_elections.APP, user):
@@ -69,7 +70,8 @@ class FlaskElectionstests(ModelFlasktests):
output = self.app.get('/vote/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data)
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
self.setup_db()
@@ -133,7 +135,8 @@ class FlaskElectionstests(ModelFlasktests):
'/vote_range/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data)
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
self.setup_db()
@@ -245,7 +248,8 @@ class FlaskElectionstests(ModelFlasktests):
'/vote_range/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data)
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
self.setup_db()
@@ -328,7 +332,8 @@ class FlaskElectionstests(ModelFlasktests):
'/vote_simple/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data)
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
self.setup_db()
@@ -408,7 +413,8 @@ class FlaskElectionstests(ModelFlasktests):
'/vote_simple/test_election', follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
- '<title>OpenID transaction in progress</title>' in output.data)
+ '<title>OpenID transaction in progress</title>' in output.data
+ or 'discoveryfailure' in output.data)
self.setup_db()
commit 50a1d88f35a9451683f76c52a7467365ae0b9a17
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:00:34 2014 +0200
Implement the election admin groups
This allow the election admin creating the election to specify multiple
FAS groups which will be able to view the results before they are made
public without having to add them to the election admin group itself
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index a7e2f24..abb74a4 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -107,6 +107,8 @@ def is_election_admin(user, election_id):
return False
if not user.cla_done or len(user.groups) < 1:
return False
+ if is_admin(user):
+ return True
admingroups = [
group.group_name
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index 93a68fe..6d568c9 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -87,11 +87,12 @@ def admin_new_election():
time(23, 59, 59))
SESSION.add(election)
- admin = models.ElectionAdminGroup(
- election=election,
- group_name=APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
- )
- SESSION.add(admin)
+ for admin_grp in form.admin_grp.data.split(','):
+ admin = models.ElectionAdminGroup(
+ election=election,
+ group_name=admin_grp.strip(),
+ )
+ SESSION.add(admin)
SESSION.commit()
@@ -138,6 +139,26 @@ def admin_edit_election(election_alias):
form.candidates_are_fasusers.data = int(
form.candidates_are_fasusers.data)
form.populate_obj(election)
+
+ admin_groups = set(election.admin_groups_list)
+
+ new_groups = set(
+ [grp.strip() for grp in form.admin_grp.data.split(',')])
+
+ # Add the new groups
+ for admin_grp in new_groups.difference(admin_groups):
+ admin = models.ElectionAdminGroup(
+ election=election,
+ group_name=admin_grp,
+ )
+ SESSION.add(admin)
+
+ # Remove the groups that were removed with this edition
+ for admin_grp in admin_groups.difference(new_groups):
+ admingrp = models.ElectionAdminGroup.by_election_id_and_name(
+ SESSION, election.id, admin_grp)
+ SESSION.delete(admingrp)
+
SESSION.commit()
fedmsgshim.publish(
topic="election.edit",
@@ -150,6 +171,7 @@ def admin_edit_election(election_alias):
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
+ form.admin_grp.data = ', '.join(election.admin_groups_list)
return flask.render_template(
'admin/election_form.html',
form=form,
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index cca7176..c066f2d 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -59,6 +59,9 @@ class ElectionForm(wtf.Form):
embargoed = wtforms.BooleanField('Embargo results?', default=True)
+ admin_grp = wtforms.TextField(
+ 'Admin groups', [wtforms.validators.optional()])
+
def __init__(form, election_id=None, *args, **kwargs):
super(ElectionForm, form).__init__(*args, **kwargs)
form._election_id = election_id
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index c1cabfc..6c31fe1 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -112,6 +112,10 @@ class Election(BASE):
)
@property
+ def admin_groups_list(self):
+ return [grp.group_name for grp in self.admin_groups]
+
+ @property
def status(self):
now = datetime.utcnow()
if now.date() < self.start_date.date():
@@ -227,6 +231,21 @@ class ElectionAdminGroup(BASE):
cls.election_id == election_id
).all()
+ @classmethod
+ def by_election_id_and_name(cls, session, election_id, group_name):
+ """ Return the ElectionAdminGroup having a specific election_id
+ and group_name.
+ """
+ return session.query(
+ cls
+ ).filter(
+ cls.election_id == election_id
+ ).filter(
+ cls.group_name == group_name
+ ).first()
+
+
+
class Candidate(BASE):
__tablename__ = 'candidates'
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index 95eb06b..dd42f1c 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -21,6 +21,7 @@
{{ render_field_in_row(form.seats_elected) }}
{{ render_field_in_row(form.candidates_are_fasusers) }}
{{ render_field_in_row(form.embargoed) }}
+ {{ render_field_in_row(form.admin_grp) }}
</table>
</fieldset>
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index 3c63681..8726599 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -15,6 +15,7 @@
<li>End date: {{ election.end_date }}</li>
<li>Number elected: {{ election.seats_elected }}</li>
<li>Election type: {{ election.voting_type }}</li>
+ <li>Admin groups: {{ election.admin_groups_list | join(', ') }}</li>
</ul>
<h3>Candidates</h3>
{% if election.candidates.all() %}
commit 1229b32187733d3cdb9f71f23830ebbd03f4ec20
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 23:16:09 2014 +0200
Fix the reverse proxy
diff --git a/fedora_elections/proxy.py b/fedora_elections/proxy.py
index 40806cc..8e2e6b1 100644
--- a/fedora_elections/proxy.py
+++ b/fedora_elections/proxy.py
@@ -55,6 +55,10 @@ class ReverseProxied(object):
if path_info.startswith(script_name):
environ['PATH_INFO'] = path_info[len(script_name):]
+ server = environ.get('HTTP_X_FORWARDED_HOST', '')
+ if server:
+ environ['HTTP_HOST'] = server
+
scheme = environ.get('HTTP_X_SCHEME', '')
if scheme:
environ['wsgi.url_scheme'] = scheme
commit 297f60697854f664bb10e5efac3d7b9ee29225c2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 11:24:57 2014 +0200
Add elections.proxy and set elections as an application behind a reverse proxy
This will ensure that flask.redirect is using ``https`` where asked.
Fixes https://fedorahosted.org/fedora-infrastructure/ticket/4369 for elections
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 4e8878f..a7e2f24 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -38,7 +38,8 @@ from fedora.client import AuthError, AppError
from fedora.client.fas2 import AccountSystem
from flask.ext.fas_openid import FAS
-import fedmsgshim
+import fedora_elections.fedmsgshim
+import fedora_elections.proxy
APP = flask.Flask(__name__)
APP.config.from_object('fedora_elections.default_config')
@@ -47,6 +48,7 @@ if 'FEDORA_ELECTIONS_CONFIG' in os.environ: # pragma: no cover
# set up FAS
FAS = FAS(APP)
+APP.wsgi_app = fedora_elections.proxy.ReverseProxied(APP.wsgi_app)
# FAS for usernames.
FAS2 = AccountSystem(
diff --git a/fedora_elections/proxy.py b/fedora_elections/proxy.py
new file mode 100644
index 0000000..40806cc
--- /dev/null
+++ b/fedora_elections/proxy.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2014 Red Hat, Inc.
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions
+# of the GNU General Public License v.2, or (at your option) any later
+# version. This program is distributed in the hope that it will be
+# useful, but WITHOUT ANY WARRANTY expressed or implied, including the
+# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+# PURPOSE. See the GNU General Public License for more details. You
+# should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public
+# License and may only be used or replicated with the express permission
+# of Red Hat, Inc.
+#
+
+'''
+Makes elections an application behind a reverse proxy and thus ensure the
+redirects are using ``https``.
+
+Source: http://flask.pocoo.org/snippets/35/ by Peter Hansen
+'''
+
+
+class ReverseProxied(object):
+ '''Wrap the application in this middleware and configure the
+ front-end server to add these headers, to let you quietly bind
+ this to a URL other than / and to an HTTP scheme that is
+ different than what is used locally.
+
+ In nginx:
+ location /myprefix {
+ proxy_pass http://192.168.0.1:5001;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Scheme $scheme;
+ proxy_set_header X-Script-Name /myprefix;
+ }
+
+ :param app: the WSGI application
+ '''
+ def __init__(self, app):
+ self.app = app
+
+ def __call__(self, environ, start_response):
+ script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
+ if script_name:
+ environ['SCRIPT_NAME'] = script_name
+ path_info = environ['PATH_INFO']
+ if path_info.startswith(script_name):
+ environ['PATH_INFO'] = path_info[len(script_name):]
+
+ scheme = environ.get('HTTP_X_SCHEME', '')
+ if scheme:
+ environ['wsgi.url_scheme'] = scheme
+ return self.app(environ, start_response)
commit ea704ad587e140f9697d271f86b58d5d6883ba69
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 3 16:06:22 2014 +0200
Remove un-used script
diff --git a/testing b/testing
deleted file mode 100755
index 76f7c3e..0000000
--- a/testing
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-nosetests $*
-
commit c43ace49c3af55a32ae2a3e5378c53e9caa68256
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 18:31:53 2014 +0200
'##' is not allowed, and @abadger is right '# #' doesn't look right either
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index a97b40c..c1cabfc 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -41,7 +41,7 @@ def create_tables(db_url, alembic_ini=None, debug=False):
BASE.metadata.create_all(engine)
# engine.execute(collection_package_create_view(driver=engine.driver))
if db_url.startswith('sqlite:'): # pragma: no cover
- # # Ignore the warning about con_record
+ # Ignore the warning about con_record
# pylint: disable=W0613
def _fk_pragma_on_connect(dbapi_con, con_record):
''' Tries to enforce referential constraints on sqlite. '''
@@ -52,7 +52,7 @@ def create_tables(db_url, alembic_ini=None, debug=False):
# then, load the Alembic configuration and generate the
# version table, "stamping" it with the most recent rev:
- # # Ignore the warning missing alembic
+ # Ignore the warning missing alembic
# pylint: disable=F0401
from alembic.config import Config
from alembic import command
commit 0696a89321072071648875ee35eaf4d84912b5dc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 18:30:04 2014 +0200
Change comment as suggested by @abadger
diff --git a/tests/test_candidate.py b/tests/test_candidate.py
index 7a39f89..3c0d6ba 100644
--- a/tests/test_candidate.py
+++ b/tests/test_candidate.py
@@ -50,7 +50,9 @@ class Candidatetests(Modeltests):
elections.session = self.session
elections.test_init_election()
- # # Election #1
+ #
+ # Election #1
+ #
obj = models.Candidate( # id:1
election_id=1,
@@ -79,7 +81,9 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
- # # Election #3
+ #
+ # Election #3
+ #
obj = models.Candidate( # id:4
election_id=3,
@@ -108,7 +112,9 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
- # # Election #5
+ #
+ # Election #5
+ #
obj = models.Candidate( # id:7
election_id=5,
@@ -137,7 +143,9 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
- # # Election #4
+ #
+ # Election #4
+ #
obj = models.Candidate( # id:10
election_id=4,
commit e3d809cae0f77f0cb2cd7a1b9ba9d1df6fdc5514
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:51:45 2014 +0200
pep8 fixes on the tests
diff --git a/tests/__init__.py b/tests/__init__.py
index 81d4ccf..2881e5d 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
"""
(c) 2014 - Copyright Pierre-Yves Chibon
diff --git a/tests/test_candidate.py b/tests/test_candidate.py
index daabe85..7a39f89 100644
--- a/tests/test_candidate.py
+++ b/tests/test_candidate.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
"""
(c) 2014 - Copyright Pierre-Yves Chibon
@@ -50,7 +50,7 @@ class Candidatetests(Modeltests):
elections.session = self.session
elections.test_init_election()
- ## Election #1
+ # # Election #1
obj = models.Candidate( # id:1
election_id=1,
@@ -79,7 +79,7 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
- ## Election #3
+ # # Election #3
obj = models.Candidate( # id:4
election_id=3,
@@ -108,7 +108,7 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
- ## Election #5
+ # # Election #5
obj = models.Candidate( # id:7
election_id=5,
@@ -137,7 +137,7 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
- ## Election #4
+ # # Election #4
obj = models.Candidate( # id:10
election_id=4,
diff --git a/tests/test_election.py b/tests/test_election.py
index 99af3b8..6b88424 100644
--- a/tests/test_election.py
+++ b/tests/test_election.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
"""
(c) 2014 - Copyright Pierre-Yves Chibon
diff --git a/tests/test_flask.py b/tests/test_flask.py
index 6414512..761ffa2 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
"""
(c) 2014 - Copyright Pierre-Yves Chibon
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index 3ca0192..7503230 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
"""
(c) 2014 - Copyright Pierre-Yves Chibon
@@ -167,7 +167,6 @@ class FlaskAdmintests(ModelFlasktests):
'csrf_token': csrf_token,
}
-
output = self.app.post('/admin/new', data=data)
self.assertEqual(output.status_code, 200)
self.assertTrue(
@@ -202,7 +201,7 @@ class FlaskAdmintests(ModelFlasktests):
'input id="shortdesc" name="shortdesc" type="text"'
in output.data)
self.assertTrue(
- '<td class="error">End date must be later than start date.</td>'
+ 'class="error">End date must be later than start date.</td>'
in output.data)
# Invalid: alias already taken
@@ -231,7 +230,6 @@ class FlaskAdmintests(ModelFlasktests):
'<td class="error">There is already another election with '
'this alias.</td>' in output.data)
-
# Invalid: shortdesc already taken
data = {
'alias': 'new_election',
@@ -284,7 +282,6 @@ class FlaskAdmintests(ModelFlasktests):
self.assertTrue(
'<p>There are no candidates.</p>' in output.data)
-
def test_admin_edit_election(self):
""" Test the admin_edit_election function. """
user = FakeUser(
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
index 06b0a7c..b1c9fe4 100644
--- a/tests/test_flask_elections.py
+++ b/tests/test_flask_elections.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
"""
(c) 2014 - Copyright Pierre-Yves Chibon
@@ -148,7 +148,7 @@ class FlaskElectionstests(ModelFlasktests):
user = FakeUser([], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get(
- '/vote_range/test_election5')#, follow_redirects=True)
+ '/vote_range/test_election5')
self.assertTrue(
'be redirected automatically to target URL: '
'<a href="/vote_simple/test_election5">' in output.data)
@@ -421,7 +421,8 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data, follow_redirects=True)
+ '/vote_simple/test_election5', data=data,
+ follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<li class="error">Invalid vote, this candidate is not '
@@ -434,7 +435,8 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data, follow_redirects=True)
+ '/vote_simple/test_election5', data=data,
+ follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<li class="error">Invalid vote, this candidate is not '
@@ -447,7 +449,8 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data, follow_redirects=True)
+ '/vote_simple/test_election5', data=data,
+ follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'<li class="error">Invalid Ballot!</li' in output.data)
@@ -459,7 +462,8 @@ class FlaskElectionstests(ModelFlasktests):
}
output = self.app.post(
- '/vote_simple/test_election5', data=data, follow_redirects=True)
+ '/vote_simple/test_election5', data=data,
+ follow_redirects=True)
self.assertEqual(output.status_code, 200)
self.assertTrue(
'class="message">Your vote has been recorded. Thank you!</'
diff --git a/tests/test_vote.py b/tests/test_vote.py
index ca2a206..09a4135 100644
--- a/tests/test_vote.py
+++ b/tests/test_vote.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
"""
(c) 2014 - Copyright Pierre-Yves Chibon
@@ -177,7 +177,6 @@ class Votetests(Modeltests):
obj = models.Candidate.by_id(self.session, 3)
self.assertEqual(obj.vote_count, 7)
-
def test_of_user_on_election(self):
""" Test the Vote.of_user_on_election function. """
self.test_init_vote()
commit d3b7e91a56dba5b5ed180f36f6dc45f4b60cb5e0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:49:19 2014 +0200
pep8 fixes on setup.py
diff --git a/setup.py b/setup.py
index 5058cbc..1821fe5 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@
Setup script
"""
-## These two lines are needed to run on EL6
+# These two lines are needed to run on EL6
__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
import pkg_resources
commit 2bbba085ea773df6281340fd44f20783bb3929be
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:49:12 2014 +0200
pep8 fixes on fedora_elections
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 2dc3360..4e8878f 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -149,7 +149,7 @@ def inject_variables():
version=__version__)
-### LIST VIEWS #############################################
+# LIST VIEWS #############################################
@APP.route('/')
def index():
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index 163bf03..93a68fe 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -205,7 +205,7 @@ def admin_add_multi_candidate(election_alias):
candidates_name = []
for entry in form.candidate.data.strip().split("|"):
candidate = entry.split("!")
- #No url
+ # No url
if len(candidate) == 1:
cand = models.Candidate(
election=election,
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
index fcdbea0..9a37e23 100644
--- a/fedora_elections/default_config.py
+++ b/fedora_elections/default_config.py
@@ -11,4 +11,3 @@ FAS_BASE_URL = 'https://admin.stg.fedoraproject.org/accounts/'
FAS_USERNAME = ''
FAS_PASSWORD = ''
FAS_CHECK_CERT = False
-
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 3548a7d..a97b40c 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -39,9 +39,9 @@ def create_tables(db_url, alembic_ini=None, debug=False):
"""
engine = create_engine(db_url, echo=debug)
BASE.metadata.create_all(engine)
- #engine.execute(collection_package_create_view(driver=engine.driver))
+ # engine.execute(collection_package_create_view(driver=engine.driver))
if db_url.startswith('sqlite:'): # pragma: no cover
- ## Ignore the warning about con_record
+ # # Ignore the warning about con_record
# pylint: disable=W0613
def _fk_pragma_on_connect(dbapi_con, con_record):
''' Tries to enforce referential constraints on sqlite. '''
@@ -52,7 +52,7 @@ def create_tables(db_url, alembic_ini=None, debug=False):
# then, load the Alembic configuration and generate the
# version table, "stamping" it with the most recent rev:
- ## Ignore the warning missing alembic
+ # # Ignore the warning missing alembic
# pylint: disable=F0401
from alembic.config import Config
from alembic import command
@@ -234,7 +234,10 @@ class Candidate(BASE):
id = sa.Column(sa.Integer, primary_key=True)
election_id = sa.Column(
sa.Integer,
- sa.ForeignKey('elections.id', ondelete='RESTRICT', onupdate='CASCADE'),
+ sa.ForeignKey(
+ 'elections.id',
+ ondelete='RESTRICT',
+ onupdate='CASCADE'),
nullable=False)
# FAS username if candidates_are_fasusers
name = sa.Column(sa.Unicode(150), nullable=False)
@@ -293,7 +296,10 @@ class Vote(BASE):
timestamp = sa.Column(sa.DateTime, nullable=False, default=safunc.now())
candidate_id = sa.Column(
sa.Integer,
- sa.ForeignKey('candidates.id', ondelete='RESTRICT', onupdate='CASCADE'),
+ sa.ForeignKey(
+ 'candidates.id',
+ ondelete='RESTRICT',
+ onupdate='CASCADE'),
nullable=False)
value = sa.Column(sa.Integer, nullable=False)
@@ -345,7 +351,7 @@ class Vote(BASE):
).filter(
cls.election_id == election_id
).filter(
- cls.value > 0
+ cls.value > 0
).count()
stats['n_votes'] = n_votes
commit 4fa078adcc84a6580f18adf886e05f52ed8128f3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:45:50 2014 +0200
Convert the boolean into integer as in the DB model (and required by postgresql)
This should fix: http://jenkins.cloud.fedoraproject.org/job/elections/3/
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index 7865a7c..163bf03 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -77,7 +77,7 @@ def admin_new_election():
seats_elected=form.seats_elected.data,
embargoed=int(form.embargoed.data),
voting_type=form.voting_type.data,
- candidates_are_fasusers=form.candidates_are_fasusers.data,
+ candidates_are_fasusers=int(form.candidates_are_fasusers.data),
fas_user=flask.g.fas_user.username,
)
@@ -135,6 +135,8 @@ def admin_edit_election(election_alias):
form = forms.ElectionForm(election.id, obj=election)
if form.validate_on_submit():
form.embargoed.data = int(form.embargoed.data)
+ form.candidates_are_fasusers.data = int(
+ form.candidates_are_fasusers.data)
form.populate_obj(election)
SESSION.commit()
fedmsgshim.publish(
commit 8532a1f4ca8ba5b23bc8b335da6475af8170ada1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:37:43 2014 +0200
Adjust the requirements file
diff --git a/requirements.txt b/requirements.txt
index 28de0aa..7bff0c9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,9 +1,11 @@
# Used for when working from a virtualenv.
# Use this file by running "$ pip install -r requirements.txt"
-flask>=0.10
+Flask>=0.10
+Flask-wtf
kitchen
python-fedora
-Flask-wtf
python-openid
python-openid-cla
python-openid-teams
+SQLAlchemy
+wtforms
commit 1f3974ed8270e45d000d2d60a346cc40d932b065
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:24:41 2014 +0200
Specify where to find the tests so that python setup.py test works
diff --git a/setup.py b/setup.py
index ea9b589..5058cbc 100644
--- a/setup.py
+++ b/setup.py
@@ -24,4 +24,5 @@ setup(
'python-openid', 'python-openid-teams', 'python-openid-cla',
'Flask-wtf', 'wtforms',
],
+ test_suite="tests",
)
commit 6e31ddc6bd4cf6c788e073b2f2e380ced02d7632
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:20:25 2014 +0200
Fix dependency in setup.py, the package is named kitchen not python-kitchen
diff --git a/setup.py b/setup.py
index 1b3a80a..ea9b589 100644
--- a/setup.py
+++ b/setup.py
@@ -20,7 +20,7 @@ setup(
packages=['fedora_elections'],
include_package_data=True,
install_requires=[
- 'Flask', 'SQLAlchemy>=0.7', 'python-fedora', 'python-kitchen',
+ 'Flask', 'SQLAlchemy>=0.7', 'python-fedora', 'kitchen',
'python-openid', 'python-openid-teams', 'python-openid-cla',
'Flask-wtf', 'wtforms',
],
commit 157e7ae869b9a82d9c2f86881dee58c8c55a71e8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 13:26:17 2014 +0200
Fix the database session for the forms, if we want the tests to run :)
diff --git a/tests/__init__.py b/tests/__init__.py
index cbc17d7..81d4ccf 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -42,8 +42,9 @@ sys.path.insert(0, os.path.join(os.path.dirname(
os.path.abspath(__file__)), '..'))
import fedora_elections
-import fedora_elections.elections
import fedora_elections.admin
+import fedora_elections.elections
+import fedora_elections.forms
from fedora_elections import models
@@ -129,8 +130,9 @@ class ModelFlasktests(Modeltests):
fedora_elections.APP.logger.setLevel(logging.CRITICAL)
fedora_elections.SESSION = self.session
- fedora_elections.elections.SESSION = self.session
fedora_elections.admin.SESSION = self.session
+ fedora_elections.elections.SESSION = self.session
+ fedora_elections.forms.SESSION = self.session
self.app = fedora_elections.APP.test_client()
commit 596ae1fdbd423e2a5f2697b0420efb59f78ba2e2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 13:25:43 2014 +0200
Add tests for the form fields validations when creating a new elections
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index ce66e44..3ca0192 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -64,7 +64,6 @@ class FlaskAdmintests(ModelFlasktests):
username='toshio')
with user_set(fedora_elections.APP, user):
output = self.app.get('/admin/')
- #print output.data
self.assertEqual(output.status_code, 200)
self.assertTrue('<h2>Election administration</h2>' in output.data)
@@ -89,6 +88,8 @@ class FlaskAdmintests(ModelFlasktests):
def test_admin_new_election(self):
""" Test the admin_new_election function. """
+ self.setup_db()
+
user = FakeUser(
fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
username='toshio')
@@ -151,6 +152,112 @@ class FlaskAdmintests(ModelFlasktests):
'<td class="error">This field is required.</td>'
in output.data)
+ # Invalid alias
+ data = {
+ 'alias': 'new',
+ 'shortdesc': 'new election shortdesc',
+ 'description': 'new election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=2),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': 2,
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ 'csrf_token': csrf_token,
+ }
+
+
+ output = self.app.post('/admin/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Create election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+ self.assertTrue(
+ '<td class="error">The alias cannot be <code>new</code>.</td>'
+ in output.data)
+
+ # Invalid: end_date earlier than start_date
+ data = {
+ 'alias': 'new_election',
+ 'shortdesc': 'new election shortdesc',
+ 'description': 'new election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=6),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': 2,
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/admin/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Create election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+ self.assertTrue(
+ '<td class="error">End date must be later than start date.</td>'
+ in output.data)
+
+ # Invalid: alias already taken
+ data = {
+ 'alias': 'test_election',
+ 'shortdesc': 'new election shortdesc',
+ 'description': 'new election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=6),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': 2,
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/admin/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Create election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+ self.assertTrue(
+ '<td class="error">There is already another election with '
+ 'this alias.</td>' in output.data)
+
+
+ # Invalid: shortdesc already taken
+ data = {
+ 'alias': 'new_election',
+ 'shortdesc': 'test election shortdesc',
+ 'description': 'new election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=6),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': 2,
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/admin/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Create election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+ self.assertTrue(
+ '<td class="error">There is already another election with '
+ 'this summary.</td>' in output.data)
+
# All good
data = {
'alias': 'new_election',
commit 772c496ee408ae3af649230e1e431f4be9cdc6d1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 13:24:49 2014 +0200
Add missing import and fix the validate_shortdesc to actually be used
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 43b228f..cca7176 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -1,5 +1,8 @@
-from flask.ext import wtf
+# -*- coding: utf-8 -*-
+
+import flask
import wtforms
+from flask.ext import wtf
from fedora_elections import SESSION
from fedora_elections.models import Election
@@ -60,7 +63,7 @@ class ElectionForm(wtf.Form):
super(ElectionForm, form).__init__(*args, **kwargs)
form._election_id = election_id
- def validate_summary(form, field):
+ def validate_shortdesc(form, field):
check = Election.search(SESSION, shortdesc=form.shortdesc.data)
if check:
if not (form._election_id and form._election_id == check[0].id):
commit c88d2021a1dd9540a88f2fdb5c42c684e1e66888
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 13:00:11 2014 +0200
Add a little bit of documentation in the admin tests
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
index 9ad9315..ce66e44 100644
--- a/tests/test_flask_admin.py
+++ b/tests/test_flask_admin.py
@@ -101,6 +101,7 @@ class FlaskAdmintests(ModelFlasktests):
'input id="shortdesc" name="shortdesc" type="text"'
in output.data)
+ # No csrf provided
data = {
'alias': 'new_election',
'shortdesc': 'new election shortdesc',
@@ -125,6 +126,7 @@ class FlaskAdmintests(ModelFlasktests):
csrf_token = output.data.split(
'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+ # Description missing
data = {
'alias': 'new_election',
'shortdesc': 'new election shortdesc',
@@ -149,6 +151,7 @@ class FlaskAdmintests(ModelFlasktests):
'<td class="error">This field is required.</td>'
in output.data)
+ # All good
data = {
'alias': 'new_election',
'shortdesc': 'new election shortdesc',
commit 79cc86c7ee557bd98496bce2b7a8861f77e11ce2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:56:56 2014 +0200
Fix import and session for the unit-tests for the elections and admin controllers
diff --git a/tests/__init__.py b/tests/__init__.py
index 4ac5ec0..cbc17d7 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -42,6 +42,8 @@ sys.path.insert(0, os.path.join(os.path.dirname(
os.path.abspath(__file__)), '..'))
import fedora_elections
+import fedora_elections.elections
+import fedora_elections.admin
from fedora_elections import models
@@ -120,10 +122,16 @@ class ModelFlasktests(Modeltests):
super(ModelFlasktests, self).setUp()
fedora_elections.APP.config['TESTING'] = True
+ fedora_elections.APP.config[
+ 'FEDORA_ELECTIONS_ADMIN_GROUP'] = 'elections'
fedora_elections.APP.debug = True
fedora_elections.APP.logger.handlers = []
fedora_elections.APP.logger.setLevel(logging.CRITICAL)
fedora_elections.SESSION = self.session
+
+ fedora_elections.elections.SESSION = self.session
+ fedora_elections.admin.SESSION = self.session
+
self.app = fedora_elections.APP.test_client()
commit 9beb1ce45de8f2240b5b21808877a7775c7ce51e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:56:44 2014 +0200
Add unit-tests for the admin controller
diff --git a/tests/test_flask_admin.py b/tests/test_flask_admin.py
new file mode 100644
index 0000000..9ad9315
--- /dev/null
+++ b/tests/test_flask_admin.py
@@ -0,0 +1,582 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections.admin test script
+"""
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import logging
+import unittest
+import sys
+import os
+
+from datetime import time
+from datetime import timedelta
+
+import flask
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+import fedora_elections
+from tests import ModelFlasktests, Modeltests, TODAY, FakeUser, user_set
+
+
+# pylint: disable=R0904
+class FlaskAdmintests(ModelFlasktests):
+ """ Flask application tests for the admin controller. """
+
+ def test_admin(self):
+ """ Test the admin function. """
+ output = self.app.get('/admin/')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get('/admin/', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data)
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/')
+ self.assertEqual(output.status_code, 403)
+
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/')
+ #print output.data
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>Election administration</h2>' in output.data)
+
+ def test_admin_view_election(self):
+ """ Test the admin_view_election function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/election_test/')
+ self.assertEqual(output.status_code, 404)
+
+ self.setup_db()
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<a href="/admin/test_election/edit">' in output.data)
+ self.assertTrue(
+ '<p>3 candidates found</p>' in output.data)
+
+ def test_admin_new_election(self):
+ """ Test the admin_new_election function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/new')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Create election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+
+ data = {
+ 'alias': 'new_election',
+ 'shortdesc': 'new election shortdesc',
+ 'description': 'new election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=2),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': '2',
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ }
+
+ output = self.app.post('/admin/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Create election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ data = {
+ 'alias': 'new_election',
+ 'shortdesc': 'new election shortdesc',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=2),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': '2',
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/admin/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Create election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+ self.assertTrue(
+ '<td class="error">This field is required.</td>'
+ in output.data)
+
+ data = {
+ 'alias': 'new_election',
+ 'shortdesc': 'new election shortdesc',
+ 'description': 'new election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY + timedelta(days=2),
+ 'end_date': TODAY + timedelta(days=4),
+ 'seats_elected': 2,
+ 'candidates_are_fasusers': False,
+ 'embargoed': True,
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/new', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Election "new_election" added</li>'
+ in output.data)
+ self.assertTrue(
+ '<a href="/admin/new_election/edit">' in output.data)
+ self.assertTrue(
+ '<p>There are no candidates.</p>' in output.data)
+
+
+ def test_admin_edit_election(self):
+ """ Test the admin_edit_election function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/edit')
+ self.assertEqual(output.status_code, 404)
+
+ self.setup_db()
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/edit')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Edit election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+
+ data = {
+ 'alias': 'test_election',
+ 'shortdesc': 'test election shortdesc',
+ 'description': 'test election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY - timedelta(days=10),
+ 'end_date': TODAY - timedelta(days=8),
+ 'seats_elected': '2',
+ 'candidates_are_fasusers': False,
+ 'embargoed': False,
+ }
+
+ output = self.app.post('/admin/test_election/edit', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Edit election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ data = {
+ 'alias': 'test_election',
+ 'shortdesc': 'test election shortdesc',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY - timedelta(days=10),
+ 'end_date': TODAY - timedelta(days=8),
+ 'seats_elected': '2',
+ 'candidates_are_fasusers': False,
+ 'embargoed': False,
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post('/admin/test_election/edit', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>Edit election</h2>' in output.data)
+ self.assertTrue(
+ 'input id="shortdesc" name="shortdesc" type="text"'
+ in output.data)
+ self.assertTrue(
+ '<td class="error">This field is required.</td>'
+ in output.data)
+
+ # Check election before edit
+ output = self.app.get('/admin/test_election/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<p>3 candidates found</p>' in output.data)
+ self.assertTrue('<li>Number elected: 1</li>' in output.data)
+
+ data = {
+ 'alias': 'test_election',
+ 'shortdesc': 'test election shortdesc',
+ 'description': 'test election description',
+ 'voting_type': 'simple',
+ 'url': 'https://fedoraproject.org',
+ 'start_date': TODAY - timedelta(days=10),
+ 'end_date': TODAY - timedelta(days=8),
+ 'seats_elected': '2',
+ 'candidates_are_fasusers': False,
+ 'embargoed': False,
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/edit', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Election "test_election" saved</li>'
+ in output.data)
+ # We edited the seats_elected from 1 to 2
+ self.assertTrue(
+ '<li>Number elected: 2</li>' in output.data)
+ self.assertTrue(
+ '<p>3 candidates found</p>'
+ in output.data)
+
+ def test_admin_add_candidate(self):
+ """ Test the admin_add_candidate function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/new')
+ self.assertEqual(output.status_code, 404)
+
+ self.setup_db()
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/new')
+ self.assertEqual(output.status_code, 200)
+
+ self.assertTrue('<h2>Add candidate</h2>' in output.data)
+ self.assertTrue(
+ 'input id="name" name="name" type="text"' in output.data)
+
+ data = {
+ 'name': 'pingou',
+ 'url': '',
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>Add candidate</h2>' in output.data)
+ self.assertTrue(
+ 'input id="name" name="name" type="text"' in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ data = {
+ 'name': '',
+ 'url': '',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/new', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>Add candidate</h2>' in output.data)
+ self.assertTrue(
+ 'input id="name" name="name" type="text"' in output.data)
+ self.assertTrue(
+ '<td class="error">This field is required.</td>'
+ in output.data)
+
+ data = {
+ 'name': 'pingou',
+ 'url': '',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/new', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Candidate "pingou" saved</li>'
+ in output.data)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<p>4 candidates found</p>' in output.data)
+
+ def test_admin_add_multi_candidate(self):
+ """ Test the admin_add_multi_candidate function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/new/multi')
+ self.assertEqual(output.status_code, 404)
+
+ self.setup_db()
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/new/multi')
+ self.assertEqual(output.status_code, 200)
+
+ self.assertTrue('<h2>Add candidates</h2>' in output.data)
+ self.assertTrue(
+ 'input id="candidate" name="candidate" type="text"'
+ in output.data)
+
+ data = {
+ 'candidate': 'pingou',
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/new/multi', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>Add candidates</h2>' in output.data)
+ self.assertTrue(
+ 'input id="candidate" name="candidate" type="text"'
+ in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ data = {
+ 'candidate': '',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/new/multi', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>Add candidates</h2>' in output.data)
+ self.assertTrue(
+ 'input id="candidate" name="candidate" type="text"'
+ in output.data)
+ self.assertTrue(
+ '<td class="error">This field is required.</td>'
+ in output.data)
+
+ data = {
+ 'candidate': 'pingou|patrick!https://fedoraproject.org|'
+ 'shaiton!https://fedoraproject.org!test|sochotni',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/new/multi', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Added 3 candidates</li>'
+ in output.data)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<p>6 candidates found</p>' in output.data)
+
+ def test_admin_edit_candidate(self):
+ """ Test the admin_edit_candidate function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+
+ self.setup_db()
+
+ # Election does not exist
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test/candidates/1/edit')
+ self.assertEqual(output.status_code, 404)
+
+ # Candidate does not exist
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/100/edit')
+ self.assertEqual(output.status_code, 404)
+
+ # Candidate not in election
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/5/edit')
+ self.assertEqual(output.status_code, 404)
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/1/edit')
+ self.assertEqual(output.status_code, 200)
+
+ self.assertTrue('<h2>Edit candidate</h2>' in output.data)
+ self.assertTrue(
+ 'input id="name" name="name" type="text"' in output.data)
+
+ data = {
+ 'name': 'Toshio Kuratomi',
+ 'url': 'https://fedoraproject.org/wiki/User:Toshio',
+ }
+
+ self.assertTrue('<h2>Edit candidate</h2>' in output.data)
+ self.assertTrue(
+ 'input id="name" name="name" type="text"' in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ data = {
+ 'name': '',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/1/edit', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>Edit candidate</h2>' in output.data)
+ self.assertTrue(
+ 'input id="name" name="name" type="text"' in output.data)
+ self.assertTrue(
+ '<td class="error">This field is required.</td>'
+ in output.data)
+
+ # Check election before edit
+ output = self.app.get('/admin/test_election/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<p>3 candidates found</p>' in output.data)
+ self.assertTrue('<li>Number elected: 1</li>' in output.data)
+ self.assertFalse('<li>Toshio Kuratomi' in output.data)
+
+ data = {
+ 'name': 'Toshio Kuratomi',
+ 'url': 'https://fedoraproject.org/wiki/User:Toshio',
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/1/edit', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Candidate "Toshio Kuratomi" saved</li>'
+ in output.data)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<p>3 candidates found</p>' in output.data)
+ self.assertTrue('<li>Toshio Kuratomi' in output.data)
+
+ def test_admin_delete_candidate(self):
+ """ Test the admin_delete_candidate function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+
+ self.setup_db()
+
+ # Election does not exist
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test/candidates/1/delete')
+ self.assertEqual(output.status_code, 404)
+
+ # Candidate does not exist
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/100/delete')
+ self.assertEqual(output.status_code, 404)
+
+ # Candidate not in election
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/5/delete')
+ self.assertEqual(output.status_code, 404)
+
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/admin/test_election/candidates/1/delete')
+ self.assertEqual(output.status_code, 200)
+
+ self.assertTrue('<h2>Delete candidate</h2>' in output.data)
+ self.assertTrue(
+ 'p>Are you sure you want to delete candidate "Toshio"?</p'
+ in output.data)
+
+ output = self.app.post('/admin/test_election/candidates/1/delete')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>Delete candidate</h2>' in output.data)
+ self.assertTrue(
+ 'p>Are you sure you want to delete candidate "Toshio"?</p'
+ in output.data)
+
+ csrf_token = output.data.split(
+ 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+ # Try to delete while there are votes link to this candidates
+ data = {
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election/candidates/1/delete', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<h2>test election shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<li class="error">Could not delete this candidate. Is it '
+ 'already part of an election?</li>' in output.data)
+
+ # Check election before edit
+ output = self.app.get('/admin/test_election4/')
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<p>2 candidates found</p>' in output.data)
+ self.assertTrue('<li>Number elected: 1</li>' in output.data)
+ self.assertTrue('<li>Toshio' in output.data)
+ self.assertTrue(
+ '<h2>test election 4 shortdesc</h2>' in output.data)
+
+ # Delete one candidate
+ data = {
+ 'csrf_token': csrf_token,
+ }
+
+ output = self.app.post(
+ '/admin/test_election4/candidates/10/delete', data=data,
+ follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Candidate "Toshio" deleted</li>'
+ in output.data)
+ self.assertTrue(
+ '<h2>test election 4 shortdesc</h2>' in output.data)
+ self.assertTrue('<h3>Candidates</h3>' in output.data)
+ self.assertTrue('<p>1 candidates found</p>' in output.data)
+ self.assertFalse('<li>Toshio' in output.data)
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(FlaskAdmintests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit 569a0e3d0cc8ed2e81d077b867ffbd5cdb70f33e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:55:46 2014 +0200
Update the behavior of the ForeinKey upon update or delete
And move the declaration of the relationships after the declaration of
the columns
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 8eb511e..3548a7d 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -208,11 +208,14 @@ class ElectionAdminGroup(BASE):
__tablename__ = 'electionadmins'
id = sa.Column(sa.Integer, primary_key=True)
- election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
- nullable=False)
+ election_id = sa.Column(
+ sa.Integer,
+ sa.ForeignKey('elections.id', ondelete='CASCADE', onupdate='CASCADE'),
+ nullable=False)
+ group_name = sa.Column(sa.Unicode(150), nullable=False)
+
election = relationship(
'Election', backref=backref('admin_groups', lazy='dynamic'))
- group_name = sa.Column(sa.Unicode(150), nullable=False)
@classmethod
def by_election_id(cls, session, election_id):
@@ -229,14 +232,17 @@ class Candidate(BASE):
__tablename__ = 'candidates'
id = sa.Column(sa.Integer, primary_key=True)
- election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
- nullable=False)
- election = relationship(
- 'Election', backref=backref('candidates', lazy='dynamic'))
+ election_id = sa.Column(
+ sa.Integer,
+ sa.ForeignKey('elections.id', ondelete='RESTRICT', onupdate='CASCADE'),
+ nullable=False)
# FAS username if candidates_are_fasusers
name = sa.Column(sa.Unicode(150), nullable=False)
url = sa.Column(sa.Unicode(250))
+ election = relationship(
+ 'Election', backref=backref('candidates', lazy='dynamic'))
+
def to_json(self):
''' Return a json representation of this object. '''
return dict(
@@ -261,13 +267,16 @@ class LegalVoter(BASE):
__tablename__ = 'legalvoters'
id = sa.Column(sa.Integer, primary_key=True)
- election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
- nullable=False)
+ election_id = sa.Column(
+ sa.Integer,
+ sa.ForeignKey('elections.id', ondelete='RESTRICT', onupdate='CASCADE'),
+ nullable=False)
+ group_name = sa.Column(sa.Unicode(150), nullable=False)
+
election = relationship(
'Election', backref=backref('legal_voters', lazy='dynamic'))
# special names:
# "cla + one" = cla_done + 1 non-cla group
- group_name = sa.Column(sa.Unicode(150), nullable=False)
class Vote(BASE):
@@ -280,15 +289,18 @@ class Vote(BASE):
id = sa.Column(sa.Integer, primary_key=True)
election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
nullable=False)
- election = relationship(
- 'Election', backref=backref('votes', lazy='dynamic'))
voter = sa.Column(sa.Unicode(150), nullable=False)
timestamp = sa.Column(sa.DateTime, nullable=False, default=safunc.now())
- candidate_id = sa.Column(sa.Integer, sa.ForeignKey('candidates.id'),
- nullable=False)
+ candidate_id = sa.Column(
+ sa.Integer,
+ sa.ForeignKey('candidates.id', ondelete='RESTRICT', onupdate='CASCADE'),
+ nullable=False)
+ value = sa.Column(sa.Integer, nullable=False)
+
+ election = relationship(
+ 'Election', backref=backref('votes', lazy='dynamic'))
candidate = relationship(
'Candidate', backref=backref('votes', lazy='dynamic'))
- value = sa.Column(sa.Integer, nullable=False)
@classmethod
def of_user_on_election(cls, session, user, election_id, count=False):
commit 55ce447a193d8739acf013deb8db4acda008e3cf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:55:16 2014 +0200
Handle error if someone tries to delete a candidate that already has votes
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index a943086..7865a7c 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -28,7 +28,7 @@ from datetime import datetime, time
from functools import wraps
import flask
-from sqlalchemy.orm.exc import NoResultFound
+from sqlalchemy.exc import SQLAlchemyError
from fedora_elections import fedmsgshim
from fedora_elections import forms
@@ -291,17 +291,26 @@ def admin_delete_candidate(election_alias, candidate_id):
form = forms.ConfirmationForm()
if form.validate_on_submit():
candidate_name = candidate.name
- SESSION.delete(candidate)
- SESSION.commit()
- flask.flash('Candidate "%s" deleted' % candidate_name)
- fedmsgshim.publish(
- topic="candidate.delete",
- msg=dict(
- agent=flask.g.fas_user.username,
- election=candidate.election.to_json(),
- candidate=candidate.to_json(),
+ try:
+ SESSION.delete(candidate)
+ SESSION.commit()
+ flask.flash('Candidate "%s" deleted' % candidate_name)
+ fedmsgshim.publish(
+ topic="candidate.delete",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=candidate.election.to_json(),
+ candidate=candidate.to_json(),
+ )
+ )
+ except SQLAlchemyError, err:
+ SESSION.rollback()
+ APP.logger.debug('Could not delete candidate')
+ APP.logger.exception(err)
+ flask.flash(
+ 'Could not delete this candidate. Is it already part of an '
+ 'election?', 'error'
)
- )
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
commit ba08e9cf4532b708e4af405b8a190696c9e9b90f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:54:38 2014 +0200
Add couple of candidates to the election #4
diff --git a/tests/test_candidate.py b/tests/test_candidate.py
index 5ade958..daabe85 100644
--- a/tests/test_candidate.py
+++ b/tests/test_candidate.py
@@ -137,6 +137,26 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ ## Election #4
+
+ obj = models.Candidate( # id:10
+ election_id=4,
+ name='Toshio',
+ url='https://fedoraproject.org/wiki/User:Toshio',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Candidate( # id:11
+ election_id=4,
+ name='Kevin',
+ url='https://fedoraproject.org/wiki/User:Kevin',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
def test_to_json(self):
""" Test the Candidate.to_json function. """
self.test_init_candidate()
commit b310504d02c5091a46c03559eb6682761aa23944
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:54:27 2014 +0200
Remove un-used imports
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 4e9d766..2dc3360 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -37,7 +37,6 @@ import flask
from fedora.client import AuthError, AppError
from fedora.client.fas2 import AccountSystem
from flask.ext.fas_openid import FAS
-from sqlalchemy.orm.exc import NoResultFound
import fedmsgshim
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 49386ff..8f1b1a3 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -28,7 +28,6 @@ from datetime import datetime, time
from functools import wraps
import flask
-from sqlalchemy.orm.exc import NoResultFound
from fedora_elections import fedmsgshim
from fedora_elections import forms
commit eaf911ccc37429682c79755be3098fea868c608c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 11:23:26 2014 +0200
Add unit-tests for the elections controller
diff --git a/tests/test_flask_elections.py b/tests/test_flask_elections.py
new file mode 100644
index 0000000..06b0a7c
--- /dev/null
+++ b/tests/test_flask_elections.py
@@ -0,0 +1,548 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections.elections test script
+"""
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import logging
+import unittest
+import sys
+import os
+
+from datetime import time
+from datetime import timedelta
+
+import flask
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+import fedora_elections
+from tests import ModelFlasktests, Modeltests, TODAY, FakeUser, user_set
+
+
+# pylint: disable=R0904
+class FlaskElectionstests(ModelFlasktests):
+ """ Flask application tests for the elections controller. """
+
+ def test_vote_empty(self):
+ """ Test the vote function without data. """
+ output = self.app.get('/vote/test_election')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get('/vote/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data)
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/vote/test_election')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get('/vote/test_election', follow_redirects=True)
+ self.assertTrue(
+ '"message">The election, test_election, does not exist.</'
+ in output.data)
+
+ def test_vote(self):
+ """ Test the vote function. """
+ output = self.app.get('/vote/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+
+ output = self.app.get('/vote/test_election')
+ self.assertEqual(output.status_code, 302)
+
+ # Election closed and results open
+ output = self.app.get(
+ '/vote/test_election', follow_redirects=True)
+ self.assertTrue(
+ '<li class="message">This election is closed. You have been '
+ 'redirected to the election results.</li>' in output.data)
+
+ # Election closed and results are embargoed
+ output = self.app.get(
+ '/vote/test_election2', follow_redirects=True)
+ self.assertTrue(
+ '<li class="message">This election is closed. You have been '
+ 'redirected to the election results.</li>' in output.data)
+
+ # Election still pending
+ output = self.app.get(
+ '/vote/test_election4', follow_redirects=True)
+ self.assertTrue(
+ '<li class="message">Voting has not yet started, sorry.</li>'
+ in output.data)
+
+ # Election in progress
+ output = self.app.get('/vote/test_election3')
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Election in progress
+ output = self.app.get('/vote/test_election5')
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ user = FakeUser([], username='toshio')
+ with user_set(fedora_elections.APP, user):
+
+ # Election in progress
+ output = self.app.get(
+ '/vote/test_election3', follow_redirects=True)
+ self.assertTrue(
+ 'class="message">You have already voted in the election!</'
+ in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+
+ def test_vote_range(self):
+ """ Test the vote_range function - the preview part. """
+ output = self.app.get(
+ '/vote_range/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser([], username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/vote_range/test_election3', follow_redirects=True)
+ self.assertTrue(
+ 'class="message">You have already voted in the election!</'
+ in output.data)
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/vote_range/test_election5')#, follow_redirects=True)
+ self.assertTrue(
+ 'be redirected automatically to target URL: '
+ '<a href="/vote_simple/test_election5">' in output.data)
+
+ output = self.app.get(
+ '/vote_range/test_election3')
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Invalid candidate id
+ data = {
+ '9': 1,
+ '5': 3,
+ '6': 2,
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_range/test_election3', data=data)
+ self.assertEqual(output.status_code, 400)
+
+ # Invalid vote: too low
+ data = {
+ '4': -1,
+ '5': 3,
+ '6': 2,
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_range/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Invalid vote: too high
+ data = {
+ '4': 5,
+ '5': 3,
+ '6': 2,
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_range/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Invalid vote: Not numeric
+ data = {
+ '4': 'a',
+ '5': 3,
+ '6': 2,
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_range/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Valid input
+ data = {
+ '4': 1,
+ '5': 3,
+ '6': 2,
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_range/test_election3', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 3 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="confirm" value="Submit" />'
+ in output.data)
+ self.assertTrue('<div class="vtmedcool">' in output.data)
+ self.assertTrue('<div class="vthot">' in output.data)
+ self.assertTrue('<div class="vtmedwarm">' in output.data)
+
+ def test_vote_range_process(self):
+ """ Test the vote_range function - the voting part. """
+ output = self.app.get(
+ '/vote_range/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ # Invalid candidate id
+ data = {
+ '9': 1,
+ '5': 3,
+ '6': 2,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post('/vote_range/test_election3', data=data)
+ self.assertEqual(output.status_code, 400)
+
+ # Invalid vote: too low
+ data = {
+ '4': -1,
+ '5': 3,
+ '6': 2,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_range/test_election3', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Invalid Ballot!</li' in output.data)
+
+ # Invalid vote: too high
+ data = {
+ '4': 5,
+ '5': 3,
+ '6': 2,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_range/test_election3', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Invalid Ballot!</li' in output.data)
+
+ # Invalid vote: Not numeric
+ data = {
+ '4': 'a',
+ '5': 3,
+ '6': 2,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_range/test_election3', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Invalid Ballot!</li' in output.data)
+
+ # Valid input
+ data = {
+ '4': 1,
+ '5': 3,
+ '6': 2,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_range/test_election3', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ 'class="message">Your vote has been recorded. Thank you!</'
+ in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
+
+ def test_vote_simple(self):
+ """ Test the vote_simple function - the preview part. """
+ output = self.app.get(
+ '/vote_simple/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser([], username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/vote_simple/test_election5', follow_redirects=True)
+ self.assertTrue(
+ 'class="message">You have already voted in the election!</'
+ in output.data)
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/vote_simple/test_election3')
+ self.assertTrue(
+ 'be redirected automatically to target URL: '
+ '<a href="/vote_range/test_election3">' in output.data)
+
+ output = self.app.get(
+ '/vote_simple/test_election5')
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Invalid vote: No candidate
+ data = {
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_simple/test_election5', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<li class="message">Please vote for a candidate</li>'
+ in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Invalid vote: Not numeric
+ data = {
+ 'candidate': 'a',
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_simple/test_election5', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="vote" value="Preview" />'
+ in output.data)
+
+ # Valid input
+ data = {
+ 'candidate': 7,
+ 'name': 'Preview',
+ }
+
+ output = self.app.post('/vote_simple/test_election5', data=data)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<h2>test election 5 shortdesc</h2>' in output.data)
+ self.assertTrue(
+ '<input type="submit" name="confirm" value="Submit" />'
+ in output.data)
+ self.assertTrue(
+ '<li class="message">Please confirm your vote!</li>'
+ in output.data)
+
+ def test_vote_simple_process(self):
+ """ Test the vote_simple function - the voting part. """
+ output = self.app.get(
+ '/vote_simple/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>OpenID transaction in progress</title>' in output.data)
+
+ self.setup_db()
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ # Invalid candidate id
+ data = {
+ 'candidate': 1,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_simple/test_election5', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="error">Invalid vote, this candidate is not '
+ 'listed for this election</li>' in output.data)
+
+ # Invalid vote: too low
+ data = {
+ 'candidate': -1,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_simple/test_election5', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="error">Invalid vote, this candidate is not '
+ 'listed for this election</li>' in output.data)
+
+ # Invalid vote: Not numeric
+ data = {
+ 'candidate': 'a',
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_simple/test_election5', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="error">Invalid Ballot!</li' in output.data)
+
+ # Valid input
+ data = {
+ 'candidate': 7,
+ 'name': 'Submit',
+ }
+
+ output = self.app.post(
+ '/vote_simple/test_election5', data=data, follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ 'class="message">Your vote has been recorded. Thank you!</'
+ in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
+
+ def test_election_results(self):
+ """ Test the election_results function - the preview part. """
+ output = self.app.get(
+ '/results/test_election', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ 'class="message">The election, test_election, does not exist.</'
+ in output.data)
+
+ self.setup_db()
+
+ output = self.app.get('/results/test_election')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<th title="Number of votes received">Votes</th>'
+ in output.data)
+ self.assertTrue(
+ '<h3>Some statistics about this election</h3>'
+ in output.data)
+
+ output = self.app.get(
+ '/results/test_election2', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">We are sorry. The results for this '
+ 'election cannot be viewed because they are currently embargoed '
+ 'pending formal announcement.</li>' in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+
+ user = FakeUser([], username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/results/test_election2', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">We are sorry. The results for this '
+ 'election cannot be viewed because they are currently '
+ 'embargoed pending formal announcement.</li>'
+ in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/results/test_election2', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="warning">You are only seeing this page because '
+ 'you are an admin.</li>' in output.data)
+ self.assertTrue(
+ ' <li class="warning">The results for this election are '
+ 'currently embargoed pending formal announcement.</li>'
+ in output.data)
+ self.assertTrue(
+ '<th title="Number of votes received">Votes</th>'
+ in output.data)
+ self.assertTrue(
+ '<h3>Some statistics about this election</h3>'
+ in output.data)
+
+ user = FakeUser(
+ ['gitr2spec'],
+ username='kevin')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get(
+ '/results/test_election3', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">Sorry but this election is in progress, '
+ 'you may not see its results already.</li>' in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(FlaskElectionstests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit 249339ffb694e3beb52abc004eef794d12260a4c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 11:23:11 2014 +0200
One more place where we do not want to cover FAS integration in the unit-tests
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 07dadb2..49386ff 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -316,7 +316,7 @@ def election_results(election_alias):
return safe_redirect_back()
usernamemap = {}
- if (election.candidates_are_fasusers):
+ if (election.candidates_are_fasusers): # pragma: no cover
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
commit 1814761bc9919108459bf40b5671aa9299845b79
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 07:45:08 2014 +0200
Ignore in the unit-test the possibility that get_valid_election does not return an election
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index a93cd96..07dadb2 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -290,7 +290,7 @@ def vote_simple(election_alias):
def election_results(election_alias):
election = get_valid_election(election_alias, ended=True)
- if not isinstance(election, models.Election):
+ if not isinstance(election, models.Election): # pragma: no cover
return election
elif election.embargoed:
commit 4695aa5e4027063969309f9f313f9a0a8a32abaf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 07:44:20 2014 +0200
Ignore FAS integration for the unit-tests
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index a14da8b..a93cd96 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -268,7 +268,7 @@ def vote_simple(election_alias):
next_action = 'confirm'
usernamemap = {}
- if (election.candidates_are_fasusers):
+ if (election.candidates_are_fasusers): # pragma: no cover
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
commit 56253d0b79b33ef07efcffd67f3b5cc18ca0c869
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 07:43:40 2014 +0200
Fix the simple voting, make sure the candidate submitted belong to the election
And handle case the candidate id submitted is not an integer
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 3f0fe19..a14da8b 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -228,7 +228,17 @@ def vote_simple(election_alias):
next_action = ''
form_values = flask.request.form.values()
if 'Submit' in form_values:
- candidate_id = int(flask.request.form['candidate'])
+ try:
+ candidates_id = [cand.id for cand in election.candidates]
+ candidate_id = int(flask.request.form['candidate'])
+ except ValueError:
+ flask.flash("Invalid Ballot!", "error")
+ return safe_redirect_back()
+ if candidate_id not in candidates_id:
+ flask.flash("Invalid vote, this candidate is not listed for "
+ "this election", "error")
+ return safe_redirect_back()
+
new_vote = models.Vote(
election_id=election.id,
voter=flask.g.fas_user.username,
commit dcf774637d0297f03f5bfe6b21f08ebd682815be
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 20:07:42 2014 +0200
Handle the case the id submitted is not an integer
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 73a6fbe..3f0fe19 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -244,8 +244,12 @@ def vote_simple(election_alias):
elif 'Preview' in form_values:
if ('candidate' in flask.request.form):
- flask.flash("Please confirm your vote!")
- candidate_id = int(flask.request.form['candidate'])
+ try:
+ candidate_id = int(flask.request.form['candidate'])
+ flask.flash("Please confirm your vote!")
+ except ValueError:
+ next_action = 'vote'
+ flask.flash("Invalid data")
else:
flask.flash("Please vote for a candidate")
next_action = 'vote'
commit cf7b7d0230697318d30a0db73bda30eaca54e34a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 17:47:40 2014 +0200
Ignore FAS integration for the unit-tests
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 37b84d0..73a6fbe 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -183,7 +183,7 @@ def vote_range(election_alias):
next_action = 'confirm'
usernamemap = {}
- if (election.candidates_are_fasusers):
+ if (election.candidates_are_fasusers): # pragma: no cover
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
commit 801b1e11c8ff2fe33cd43866b2bfaa069e63bdc5
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 17:47:35 2014 +0200
In case the input is invalid (not an int), the next_action is vote
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index d792802..37b84d0 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -176,9 +176,10 @@ def vote_range(election_alias):
uvotes[candidate.id] = 0
next_action = 'vote'
except ValueError:
+ next_action = 'vote'
flask.flash("Invalid data")
- if (next_action != 'vote'):
+ if next_action != 'vote':
next_action = 'confirm'
usernamemap = {}
commit bcd0b9f47aba6f5bc6090c1116bc002759828276
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 17:31:18 2014 +0200
Fix typo in the Preview (in case of wrong input)
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 26e476c..d792802 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -173,7 +173,7 @@ def vote_range(election_alias):
uvotes[candidate.id] = vote
else:
flask.flash("Invalid data2.")
- uvotes[c.id] = 0
+ uvotes[candidate.id] = 0
next_action = 'vote'
except ValueError:
flask.flash("Invalid data")
commit 71ea100fd4506769868129121d1a58f90a42b09c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:54:47 2014 +0200
Ignore case where we would not get a valid election object from get_valid_election
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 55522b2..26e476c 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -108,7 +108,7 @@ def vote(election_alias):
def vote_range(election_alias):
election = get_valid_election(election_alias)
- if not isinstance(election, models.Election):
+ if not isinstance(election, models.Election): # pragma: no cover
return election
if (election.voting_type == 'simple'):
@@ -205,7 +205,7 @@ def vote_range(election_alias):
def vote_simple(election_alias):
election = get_valid_election(election_alias)
- if not isinstance(election, models.Election):
+ if not isinstance(election, models.Election): # pragma: no cover
return election
if (election.voting_type == 'range'):
commit 939abe3e9d1cfaa000b8e6cb0e43ffb2c1938d5d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:54:03 2014 +0200
Adjust the candidate.id for the candidates that were added to elections #3
diff --git a/tests/test_vote.py b/tests/test_vote.py
index 4ee61e1..ca2a206 100644
--- a/tests/test_vote.py
+++ b/tests/test_vote.py
@@ -137,7 +137,7 @@ class Votetests(Modeltests):
obj = models.Vote( # id:1
election_id=5,
voter='toshio',
- candidate_id=4,
+ candidate_id=7,
value='1',
)
self.session.add(obj)
@@ -147,7 +147,7 @@ class Votetests(Modeltests):
obj = models.Vote( # id:2
election_id=5,
voter='toshio',
- candidate_id=5,
+ candidate_id=8,
value='1',
)
self.session.add(obj)
@@ -157,7 +157,7 @@ class Votetests(Modeltests):
obj = models.Vote( # id:4
election_id=5,
voter='kevin',
- candidate_id=6,
+ candidate_id=9,
value='1',
)
self.session.add(obj)
commit 1e20199ea95c8d9bee4afb438813cc3832c5b626
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:53:44 2014 +0200
Add candidates to the election #3 and adjust the candidate.id
diff --git a/tests/test_candidate.py b/tests/test_candidate.py
index 9883485..5ade958 100644
--- a/tests/test_candidate.py
+++ b/tests/test_candidate.py
@@ -79,10 +79,10 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
- ## Election #5
+ ## Election #3
obj = models.Candidate( # id:4
- election_id=5,
+ election_id=3,
name='Toshio',
url='https://fedoraproject.org/wiki/User:Toshio',
)
@@ -91,7 +91,7 @@ class Candidatetests(Modeltests):
self.assertNotEqual(obj, None)
obj = models.Candidate( # id:5
- election_id=5,
+ election_id=3,
name='Kevin',
url='https://fedoraproject.org/wiki/User:Kevin',
)
@@ -100,6 +100,35 @@ class Candidatetests(Modeltests):
self.assertNotEqual(obj, None)
obj = models.Candidate( # id:6
+ election_id=3,
+ name='Ralph',
+ url='https://fedoraproject.org/wiki/User:Ralph',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ ## Election #5
+
+ obj = models.Candidate( # id:7
+ election_id=5,
+ name='Toshio',
+ url='https://fedoraproject.org/wiki/User:Toshio',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Candidate( # id:8
+ election_id=5,
+ name='Kevin',
+ url='https://fedoraproject.org/wiki/User:Kevin',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Candidate( # id:9
election_id=5,
name='Ralph',
url='https://fedoraproject.org/wiki/User:Ralph',
commit 6f9cd7c2486d194355c0bcde9f835a7479db1946
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:43:57 2014 +0200
Ignore case where we don't know the election's voting type for the unit-tests
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 98a30ed..55522b2 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -97,7 +97,7 @@ def vote(election_alias):
return vote_range(election_alias)
elif election.voting_type == 'simple':
return vote_simple(election_alias)
- else:
+ else: # pragma: no cover
flask.flash(
'Unknown election voting type: %s' % election.voting_type)
return safe_redirect_back()
commit a38e0a6b983ea1ebe466e210f619921885e3bfda
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:39:03 2014 +0200
Adjust the flask tests to new election added
diff --git a/tests/test_flask.py b/tests/test_flask.py
index c4100c3..6414512 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -249,7 +249,7 @@ class Flasktests(ModelFlasktests):
output = self.app.get('/open')
self.assertEqual(output.status_code, 200)
self.assertTrue('<title>Fedora elections</title>' in output.data)
- self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 2 elections</h3>' in output.data)
self.assertTrue('<td>test election 3 shortdesc</td>' in output.data)
self.assertTrue('<a href="/vote_range/' in output.data)
self.assertTrue('<a href="/login">login</a>' in output.data)
@@ -259,7 +259,7 @@ class Flasktests(ModelFlasktests):
output = self.app.get('/open')
self.assertEqual(output.status_code, 200)
self.assertTrue('<title>Fedora elections</title>' in output.data)
- self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 2 elections</h3>' in output.data)
self.assertTrue('<a href="/vote_range/' in output.data)
self.assertTrue(
'<span class="text">logged in as </span>' in output.data)
@@ -270,7 +270,7 @@ class Flasktests(ModelFlasktests):
output = self.app.get('/open')
self.assertEqual(output.status_code, 200)
self.assertTrue('<title>Fedora elections</title>' in output.data)
- self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 2 elections</h3>' in output.data)
self.assertTrue(
'<span class="text">logged in as </span>' in output.data)
self.assertFalse('Vote now!' in output.data)
commit cb3c3c561a9c809c835488ec985eb4287fa3b809
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:38:40 2014 +0200
Add missing shebang to the runserver.py script
diff --git a/runserver.py b/runserver.py
index f5ce1d9..5f21658 100755
--- a/runserver.py
+++ b/runserver.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
import __main__
__main__.__requires__ = ['SQLAlchemy >= 0.7']
import pkg_resources
commit fb2a284710179a9ae204db7f646b3a36606c4885
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:38:19 2014 +0200
Add votes to the new election #5 with simple voting
diff --git a/tests/test_vote.py b/tests/test_vote.py
index 86908f7..4ee61e1 100644
--- a/tests/test_vote.py
+++ b/tests/test_vote.py
@@ -132,6 +132,38 @@ class Votetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ # Election 5
+
+ obj = models.Vote( # id:1
+ election_id=5,
+ voter='toshio',
+ candidate_id=4,
+ value='1',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:2
+ election_id=5,
+ voter='toshio',
+ candidate_id=5,
+ value='1',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:4
+ election_id=5,
+ voter='kevin',
+ candidate_id=6,
+ value='1',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
def test_vote_count_with_votes(self):
""" Test the Candidate.vote_count function with votes in. """
self.test_init_vote()
commit 19ccf601ef911bf82bee1e104f7cb9c2aa74da87
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:34:38 2014 +0200
Add candidates to the new election having simple voting
diff --git a/tests/test_candidate.py b/tests/test_candidate.py
index 2d7b47d..9883485 100644
--- a/tests/test_candidate.py
+++ b/tests/test_candidate.py
@@ -50,6 +50,8 @@ class Candidatetests(Modeltests):
elections.session = self.session
elections.test_init_election()
+ ## Election #1
+
obj = models.Candidate( # id:1
election_id=1,
name='Toshio',
@@ -77,6 +79,35 @@ class Candidatetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ ## Election #5
+
+ obj = models.Candidate( # id:4
+ election_id=5,
+ name='Toshio',
+ url='https://fedoraproject.org/wiki/User:Toshio',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Candidate( # id:5
+ election_id=5,
+ name='Kevin',
+ url='https://fedoraproject.org/wiki/User:Kevin',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Candidate( # id:6
+ election_id=5,
+ name='Ralph',
+ url='https://fedoraproject.org/wiki/User:Ralph',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
def test_to_json(self):
""" Test the Candidate.to_json function. """
self.test_init_candidate()
commit 6906bfd5698d004e3d5b84d491810162971e098b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:33:55 2014 +0200
Add a new simple and open election and adjust the tests to this election
diff --git a/tests/test_election.py b/tests/test_election.py
index 73709d2..99af3b8 100644
--- a/tests/test_election.py
+++ b/tests/test_election.py
@@ -117,6 +117,24 @@ class Electiontests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ # Election - simple voting - Open
+ obj = models.Election( # id:5
+ shortdesc='test election 5 shortdesc',
+ alias='test_election5',
+ description='test election 5 description',
+ url='https://fedoraproject.org',
+ start_date=TODAY - timedelta(days=1),
+ end_date=TODAY + timedelta(days=3),
+ seats_elected=1,
+ embargoed=1,
+ voting_type='simple',
+ candidates_are_fasusers=0,
+ fas_user='skvidal',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
def test_get_election(self):
""" Test the Election.get function. """
self.test_init_election()
@@ -182,17 +200,19 @@ class Electiontests(Modeltests):
self.session, fas_user='skvidal')
self.assertNotEqual(obj, None)
self.assertNotEqual(obj, [])
- self.assertEqual(len(obj), 1)
+ self.assertEqual(len(obj), 2)
self.assertEqual(obj[0].description, 'test election 4 description')
+ self.assertEqual(obj[1].description, 'test election 5 description')
obj = models.Election.search(self.session)
self.assertNotEqual(obj, None)
self.assertNotEqual(obj, [])
- self.assertEqual(len(obj), 4)
+ self.assertEqual(len(obj), 5)
self.assertEqual(obj[0].description, 'test election 4 description')
- self.assertEqual(obj[1].description, 'test election 3 description')
- self.assertEqual(obj[2].description, 'test election 2 description')
- self.assertEqual(obj[3].description, 'test election description')
+ self.assertEqual(obj[1].description, 'test election 5 description')
+ self.assertEqual(obj[2].description, 'test election 3 description')
+ self.assertEqual(obj[3].description, 'test election 2 description')
+ self.assertEqual(obj[4].description, 'test election description')
def test_get_older_election(self):
""" Test the Election.get_older_election function. """
@@ -210,8 +230,9 @@ class Electiontests(Modeltests):
obj = models.Election.get_open_election(self.session, limit=TODAY)
self.assertNotEqual(obj, None)
self.assertNotEqual(obj, [])
- self.assertEqual(len(obj), 1)
- self.assertEqual(obj[0].shortdesc, 'test election 3 shortdesc')
+ self.assertEqual(len(obj), 2)
+ self.assertEqual(obj[0].shortdesc, 'test election 5 shortdesc')
+ self.assertEqual(obj[1].shortdesc, 'test election 3 shortdesc')
def test_get_next_election(self):
""" Test the Election.get_next_election function. """
commit 1846b24953a89e8bb9c5b2985c86dade1d9d1e39
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:21:46 2014 +0200
Add missing import in the elections controller
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 825cc82..98a30ed 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -34,8 +34,8 @@ from fedora_elections import fedmsgshim
from fedora_elections import forms
from fedora_elections import models
from fedora_elections import (
- APP, SESSION, is_authenticated, is_admin, is_safe_url,
- safe_redirect_back,
+ APP, SESSION, is_authenticated, is_admin, is_election_admin,
+ is_safe_url, safe_redirect_back,
)
commit 2b45fe92c9494f8aa9c9c7241c2a3c1bc74a11b8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:21:08 2014 +0200
Add missing spaces in flashed messages
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 5538357..825cc82 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -280,7 +280,7 @@ def election_results(election_alias):
elif election.embargoed:
if not hasattr(flask.g, 'fas_user') or not flask.g.fas_user:
- flask.flash("We are sorry. The results for this election"
+ flask.flash("We are sorry. The results for this election "
"cannot be viewed because they are currently "
"embargoed pending formal announcement.")
return safe_redirect_back()
@@ -295,7 +295,7 @@ def election_results(election_alias):
pass
else:
flask.flash(
- "We are sorry. The results for this election"
+ "We are sorry. The results for this election "
"cannot be viewed because they are currently "
"embargoed pending formal announcement.")
return safe_redirect_back()
commit 9bc0fb895495f88256a42dea9bae7791ded0e53d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:20:14 2014 +0200
Remove double spaces on a flashed message
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 184fd3c..5538357 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -55,7 +55,7 @@ def get_valid_election(election_alias, ended=False):
election = models.Election.get(SESSION, alias=election_alias)
if not election:
- flask.flash('The election, %s, does not exist.' % election_alias)
+ flask.flash('The election, %s, does not exist.' % election_alias)
return safe_redirect_back()
if election.status == 'Pending':
commit caa1712c8bc3014f2475c335ef3c4006f31201f1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:19:04 2014 +0200
Do not show voting page on election embargoed, these are just ended embargoed
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index 0afb02e..184fd3c 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -62,7 +62,7 @@ def get_valid_election(election_alias, ended=False):
flask.flash('Voting has not yet started, sorry.')
return safe_redirect_back()
- elif not ended and election.status == 'Ended':
+ elif not ended and election.status in ('Ended', 'Embargoed'):
flask.flash(
'This election is closed. You have been redirected to the '
'election results.')
commit 72594ddd7175123e9af82c7fda02883b1796b418
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:05:14 2014 +0200
Update the auth_login method to remove duplicated checks
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 96a0d73..4e9d766 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -245,8 +245,6 @@ def open_elections():
@APP.route('/login', methods=('GET', 'POST'))
def auth_login():
- if flask.g.fas_user:
- return safe_redirect_back()
next_url = None
if 'next' in flask.request.args:
if is_safe_url(flask.request.args['next']):
@@ -256,7 +254,7 @@ def auth_login():
next_url = flask.url_for('.index')
if hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None:
- return flask.redirect(next_url)
+ return safe_redirect_back(next_url)
else:
return FAS.login(return_url=next_url)
commit 6639c107b827bfd3bda92077704094b96363011c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:56:03 2014 +0200
Port the flask applications to the ModelFlasktests class
diff --git a/tests/test_flask.py b/tests/test_flask.py
index 444d53f..c4100c3 100644
--- a/tests/test_flask.py
+++ b/tests/test_flask.py
@@ -23,7 +23,6 @@
__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
import pkg_resources
-import logging
import unittest
import sys
import os
@@ -37,32 +36,13 @@ sys.path.insert(0, os.path.join(os.path.dirname(
os.path.abspath(__file__)), '..'))
import fedora_elections
-from tests import Modeltests, TODAY, FakeUser, user_set
+from tests import ModelFlasktests, FakeUser, user_set
# pylint: disable=R0904
-class Flasktests(Modeltests):
+class Flasktests(ModelFlasktests):
""" Flask application tests. """
- def __setup_db(self):
- """ Add a calendar and some meetings so that we can play with
- something. """
- from test_vote import Votetests
- votes = Votetests('test_init_vote')
- votes.session = self.session
- votes.test_init_vote()
-
- def setUp(self):
- """ Set up the environnment, ran before every tests. """
- super(Flasktests, self).setUp()
-
- fedora_elections.APP.config['TESTING'] = True
- fedora_elections.APP.debug = True
- fedora_elections.APP.logger.handlers = []
- fedora_elections.APP.logger.setLevel(logging.CRITICAL)
- fedora_elections.SESSION = self.session
- self.app = fedora_elections.APP.test_client()
-
def test_index_empty(self):
""" Test the index function. """
output = self.app.get('/')
@@ -83,7 +63,7 @@ class Flasktests(Modeltests):
def test_index_filled(self):
""" Test the index function. """
- self.__setup_db()
+ self.setup_db()
output = self.app.get('/')
self.assertEqual(output.status_code, 200)
self.assertTrue('<title>Fedora elections</title>' in output.data)
@@ -164,7 +144,7 @@ class Flasktests(Modeltests):
output = self.app.get('/login?next=http://localhost/')
self.assertEqual(output.status_code, 200)
- self.__setup_db()
+ self.setup_db()
user = FakeUser([], username='pingou')
with user_set(fedora_elections.APP, user):
output = self.app.get('/login', follow_redirects=True)
@@ -194,7 +174,7 @@ class Flasktests(Modeltests):
def test_about_election(self):
""" Test the about_election function. """
- self.__setup_db()
+ self.setup_db()
output = self.app.get('/about/blah')
self.assertEqual(output.status_code, 302)
@@ -237,7 +217,7 @@ class Flasktests(Modeltests):
self.assertTrue('<title>Fedora elections</title>' in output.data)
self.assertTrue('<h2>Elections</h2>' in output.data)
- self.__setup_db()
+ self.setup_db()
output = self.app.get('/archives')
self.assertEqual(output.status_code, 200)
@@ -264,7 +244,7 @@ class Flasktests(Modeltests):
self.assertTrue('<title>Fedora elections</title>' in output.data)
self.assertTrue('<h2>Elections</h2>' in output.data)
- self.__setup_db()
+ self.setup_db()
output = self.app.get('/open')
self.assertEqual(output.status_code, 200)
commit ba32398615a4706ff92921d89402390beefed0d0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:55:43 2014 +0200
Add a generic unit-test class for the Flask tests
diff --git a/tests/__init__.py b/tests/__init__.py
index 59e94fe..4ac5ec0 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -24,6 +24,7 @@
__requires__ = ['SQLAlchemy >= 0.7']
import pkg_resources
+import logging
import unittest
import sys
import os
@@ -40,6 +41,7 @@ from sqlalchemy.orm import scoped_session
sys.path.insert(0, os.path.join(os.path.dirname(
os.path.abspath(__file__)), '..'))
+import fedora_elections
from fedora_elections import models
@@ -102,6 +104,29 @@ class Modeltests(unittest.TestCase):
requests.get('%s/clean/%s' % (FAITOUT_URL, db_name))
+class ModelFlasktests(Modeltests):
+ """ Model flask application tests. """
+
+ def setup_db(self):
+ """ Add a calendar and some meetings so that we can play with
+ something. """
+ from test_vote import Votetests
+ votes = Votetests('test_init_vote')
+ votes.session = self.session
+ votes.test_init_vote()
+
+ def setUp(self):
+ """ Set up the environnment, ran before every tests. """
+ super(ModelFlasktests, self).setUp()
+
+ fedora_elections.APP.config['TESTING'] = True
+ fedora_elections.APP.debug = True
+ fedora_elections.APP.logger.handlers = []
+ fedora_elections.APP.logger.setLevel(logging.CRITICAL)
+ fedora_elections.SESSION = self.session
+ self.app = fedora_elections.APP.test_client()
+
+
class FakeGroup(object):
""" Fake object used to make the FakeUser object closer to the
expectations.
commit ebdd775278cd664ae9f6e69e1f5d6ed66319851f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:23:40 2014 +0200
Ignore fedmsg in the unit-tests
diff --git a/fedora_elections/fedmsgshim.py b/fedora_elections/fedmsgshim.py
index 2e13398..785fc90 100644
--- a/fedora_elections/fedmsgshim.py
+++ b/fedora_elections/fedmsgshim.py
@@ -9,7 +9,7 @@ send messages. If it is not installed, it will return silently.
import warnings
-def publish(*args, **kwargs):
+def publish(*args, **kwargs): # pragma: no cover
try:
import fedmsg
fedmsg.publish(*args, **kwargs)
commit 64ee446ad9f78a875027ea84b2422d660cd8c858
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:22:05 2014 +0200
Add unit-tests for the generic flask endpoints
diff --git a/tests/test_flask.py b/tests/test_flask.py
new file mode 100644
index 0000000..444d53f
--- /dev/null
+++ b/tests/test_flask.py
@@ -0,0 +1,301 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections.model.Election test script
+"""
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import logging
+import unittest
+import sys
+import os
+
+from datetime import time
+from datetime import timedelta
+
+import flask
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+import fedora_elections
+from tests import Modeltests, TODAY, FakeUser, user_set
+
+
+# pylint: disable=R0904
+class Flasktests(Modeltests):
+ """ Flask application tests. """
+
+ def __setup_db(self):
+ """ Add a calendar and some meetings so that we can play with
+ something. """
+ from test_vote import Votetests
+ votes = Votetests('test_init_vote')
+ votes.session = self.session
+ votes.test_init_vote()
+
+ def setUp(self):
+ """ Set up the environnment, ran before every tests. """
+ super(Flasktests, self).setUp()
+
+ fedora_elections.APP.config['TESTING'] = True
+ fedora_elections.APP.debug = True
+ fedora_elections.APP.logger.handlers = []
+ fedora_elections.APP.logger.setLevel(logging.CRITICAL)
+ fedora_elections.SESSION = self.session
+ self.app = fedora_elections.APP.test_client()
+
+ def test_index_empty(self):
+ """ Test the index function. """
+ output = self.app.get('/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertFalse('<h3>Current elections</h3>' in output.data)
+ self.assertFalse('<h3>Next' in output.data)
+ self.assertFalse('<h3>Last' in output.data)
+ self.assertTrue('<a href="/login">login</a>' in output.data)
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue(
+ '<span class="text">logged in as </span>' in output.data)
+
+ def test_index_filled(self):
+ """ Test the index function. """
+ self.__setup_db()
+ output = self.app.get('/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
+ self.assertTrue('<a href="/login">login</a>' in output.data)
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
+ self.assertTrue('<a href="/vote_range/' in output.data)
+ self.assertTrue(
+ '<span class="text">logged in as </span>' in output.data)
+ self.assertTrue('Vote now!' in output.data)
+
+ user = FakeUser([], username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
+ self.assertTrue(
+ '<span class="text">logged in as </span>' in output.data)
+ self.assertFalse('Vote now!' in output.data)
+
+ def test_is_admin(self):
+ """ Test the is_admin function. """
+ app = flask.Flask('fedora_elections')
+
+ with app.test_request_context():
+ flask.g.fas_user = None
+ self.assertFalse(fedora_elections.is_admin(flask.g.fas_user))
+
+ flask.g.fas_user = FakeUser()
+ self.assertFalse(fedora_elections.is_admin(flask.g.fas_user))
+
+ flask.g.fas_user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'])
+ self.assertTrue(fedora_elections.is_admin(flask.g.fas_user))
+
+ def test_is_election_admin(self):
+ """ Test the is_election_admin function. """
+ app = flask.Flask('fedora_elections')
+
+ with app.test_request_context():
+ flask.g.fas_user = None
+ self.assertFalse(
+ fedora_elections.is_election_admin(
+ flask.g.fas_user, 1)
+ )
+ flask.g.fas_user = FakeUser()
+ self.assertFalse(
+ fedora_elections.is_election_admin(
+ flask.g.fas_user, 1)
+ )
+ flask.g.fas_user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'])
+ self.assertFalse(fedora_elections.is_election_admin(
+ flask.g.fas_user, 1))
+
+ def test_auth_login(self):
+ """ Test the auth_login function. """
+ app = flask.Flask('fedora_elections')
+
+ with app.test_request_context():
+ flask.g.fas_user = FakeUser(['gitr2spec'])
+ output = self.app.get('/login')
+ self.assertEqual(output.status_code, 200)
+
+ output = self.app.get('/login?next=http://localhost/')
+ self.assertEqual(output.status_code, 200)
+
+ self.__setup_db()
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/login', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+
+ def test_auth_logout(self):
+ """ Test the auth_logout function. """
+ user = FakeUser(
+ fedora_elections.APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'])
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/logout', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue(
+ '<li class="message">You have been logged out</li>'
+ in output.data)
+
+ user = None
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/logout', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertFalse(
+ '<li class="message">You have been logged out</li>'
+ in output.data)
+
+ def test_about_election(self):
+ """ Test the about_election function. """
+ self.__setup_db()
+
+ output = self.app.get('/about/blah')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get('/about/blah', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">The election, blah, does not exist.</li>'
+ in output.data)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h3>Current elections</h3>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<h3>Last 2 elections</h3>' in output.data)
+
+ output = self.app.get('/about/test_election')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<title>Election Information</title>' in output.data)
+ self.assertTrue(
+ '<small><a href="https://fedoraproject.org/wiki/User:Toshio">'
+ '[info]</a></small>' in output.data)
+ self.assertTrue(
+ '<small><a href="https://fedoraproject.org/wiki/User:Ralph">'
+ '[info]</a></small>' in output.data)
+ self.assertTrue(
+ '<a href="/results/test_election">Show Results!</a>'
+ in output.data)
+ self.assertTrue('<a href="/login">login</a>' in output.data)
+
+ def test_archived_election(self):
+ """ Test the archived_elections function. """
+ output = self.app.get('/archives')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get('/archives', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">There are no archived elections.</li>'
+ in output.data)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h2>Elections</h2>' in output.data)
+
+ self.__setup_db()
+
+ output = self.app.get('/archives')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h2>Past Elections</h2>' in output.data)
+ self.assertTrue(
+ '<td>test election 2 description</td>' in output.data)
+ self.assertTrue(
+ '<td>test election description</td>' in output.data)
+ self.assertEqual(output.data.count('<a href="results/'), 2)
+ self.assertEqual(output.data.count('<a href="about/'), 2)
+ self.assertTrue('<a href="/login">login</a>' in output.data)
+
+ def test_open_elections(self):
+ """ Test the open_elections function. """
+ output = self.app.get('/open')
+ self.assertEqual(output.status_code, 302)
+
+ output = self.app.get('/open', follow_redirects=True)
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue(
+ '<li class="message">There are no open elections.</li>'
+ in output.data)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h2>Elections</h2>' in output.data)
+
+ self.__setup_db()
+
+ output = self.app.get('/open')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<td>test election 3 shortdesc</td>' in output.data)
+ self.assertTrue('<a href="/vote_range/' in output.data)
+ self.assertTrue('<a href="/login">login</a>' in output.data)
+
+ user = FakeUser([], username='pingou')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/open')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue('<a href="/vote_range/' in output.data)
+ self.assertTrue(
+ '<span class="text">logged in as </span>' in output.data)
+ self.assertTrue('Vote now!' in output.data)
+
+ user = FakeUser([], username='toshio')
+ with user_set(fedora_elections.APP, user):
+ output = self.app.get('/open')
+ self.assertEqual(output.status_code, 200)
+ self.assertTrue('<title>Fedora elections</title>' in output.data)
+ self.assertTrue('<h3>Next 1 elections</h3>' in output.data)
+ self.assertTrue(
+ '<span class="text">logged in as </span>' in output.data)
+ self.assertFalse('Vote now!' in output.data)
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(Flasktests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit a108c762c6e76d939a7e43d418ae7735546a4b8d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:21:11 2014 +0200
Add some more votes on the election #3
This way we have someone that voted on an open election
diff --git a/tests/test_vote.py b/tests/test_vote.py
index 9aaccac..86908f7 100644
--- a/tests/test_vote.py
+++ b/tests/test_vote.py
@@ -100,18 +100,51 @@ class Votetests(Modeltests):
self.session.commit()
self.assertNotEqual(obj, None)
+ # Election 3
+
+ obj = models.Vote( # id:1
+ election_id=3,
+ voter='toshio',
+ candidate_id=1,
+ value='3',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:2
+ election_id=3,
+ voter='toshio',
+ candidate_id=2,
+ value='3',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:4
+ election_id=3,
+ voter='toshio',
+ candidate_id=3,
+ value='3',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
def test_vote_count_with_votes(self):
""" Test the Candidate.vote_count function with votes in. """
self.test_init_vote()
obj = models.Candidate.get(self.session, 2)
- self.assertEqual(obj.vote_count, 5)
+ self.assertEqual(obj.vote_count, 8)
+
+ obj = models.Candidate.get(self.session, 1)
+ self.assertEqual(obj.vote_count, 6)
obj = models.Candidate.by_id(self.session, 3)
- self.assertEqual(obj.vote_count, 4)
+ self.assertEqual(obj.vote_count, 7)
- obj = models.Candidate.get(self.session, 1)
- self.assertEqual(obj.vote_count, 3)
def test_of_user_on_election(self):
""" Test the Vote.of_user_on_election function. """
commit b750e792e6c53d7e0e60ef6d61895f5bd71b44d7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:20:50 2014 +0200
Ignore some other lines in the unit-tests coverage report
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 53ecf9b..96a0d73 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -127,10 +127,10 @@ def is_safe_url(target):
def safe_redirect_back(next=None, fallback=('index', {})):
''' Safely redirect the user to its previous page. '''
targets = []
- if next:
+ if next: # pragma: no cover
targets.append(next)
if 'next' in flask.request.args and \
- flask.request.args['next']:
+ flask.request.args['next']: # pragma: no cover
targets.append(flask.request.args['next'])
targets.append(flask.url_for(fallback[0], **fallback[1]))
for target in targets:
@@ -187,7 +187,7 @@ def about_election(election_alias):
return safe_redirect_back()
usernamemap = {}
- if (election.candidates_are_fasusers):
+ if (election.candidates_are_fasusers): # pragma: no cover
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
commit ca7698490bfb39222b2ec0c1c37225f8c03fd1d2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 14:44:27 2014 +0200
Add missing docstrings
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 966eb37..53ecf9b 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -117,6 +117,7 @@ def is_election_admin(user, election_id):
def is_safe_url(target):
+ ''' Check is a url is safe to use or not. '''
ref_url = urlparse(flask.request.host_url)
test_url = urlparse(urljoin(flask.request.host_url, target))
return test_url.scheme in ('http', 'https') and \
@@ -124,6 +125,7 @@ def is_safe_url(target):
def safe_redirect_back(next=None, fallback=('index', {})):
+ ''' Safely redirect the user to its previous page. '''
targets = []
if next:
targets.append(next)
commit 9d1b876057a999690572690dad486f9699a2603c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 14:42:39 2014 +0200
Ignore reading the config file specified by environment variable
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 9873635..966eb37 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -43,7 +43,7 @@ import fedmsgshim
APP = flask.Flask(__name__)
APP.config.from_object('fedora_elections.default_config')
-if 'FEDORA_ELECTIONS_CONFIG' in os.environ:
+if 'FEDORA_ELECTIONS_CONFIG' in os.environ: # pragma: no cover
APP.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
# set up FAS
commit ae9feacd4c045fc03ee8664993c18db210e0e30b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:28:29 2014 +0200
Ignore the .coverage file created when running the tests with coverage
diff --git a/.gitignore b/.gitignore
index da17908..fc2dc27 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
*.egg-info
build/
dist/
+.coverage
commit 8c8dac6c0d39a74500284f5f1f71f4bac1ac8a3c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:27:54 2014 +0200
Add the runtest.sh script for easy testings of the project
diff --git a/runtests.sh b/runtests.sh
new file mode 100755
index 0000000..47f97e4
--- /dev/null
+++ b/runtests.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+PYTHONPATH=fedora_elections ./nosetests \
+--with-coverage --cover-erase --cover-package=fedora_elections $*
commit f1c7aa44b2d4d67b2a926f04844475475d49b512
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:27:32 2014 +0200
Add a local nosetests script ensuring we run the proper version of the libraries
diff --git a/nosetests b/nosetests
new file mode 100755
index 0000000..42c36f1
--- /dev/null
+++ b/nosetests
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# EASY-INSTALL-ENTRY-SCRIPT: 'nose==1.3.0','console_scripts','nosetests'
+__requires__ = ['nose==1.3.0', 'flask>=0.10']
+import sys
+from pkg_resources import load_entry_point
+
+if __name__ == '__main__':
+ sys.exit(
+ load_entry_point('nose==1.3.0', 'console_scripts', 'nosetests')()
+ )
commit fbbf712917b8fba53975c6ed4a39d93965c67849
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:27:17 2014 +0200
Add unit-tests for the Vote object
diff --git a/tests/test_vote.py b/tests/test_vote.py
new file mode 100644
index 0000000..9aaccac
--- /dev/null
+++ b/tests/test_vote.py
@@ -0,0 +1,150 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections.model.Vote test script
+"""
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import unittest
+import sys
+import os
+
+from datetime import time
+from datetime import timedelta
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+from fedora_elections import models
+from tests import Modeltests, TODAY
+from test_candidate import Candidatetests
+
+
+# pylint: disable=R0904
+class Votetests(Modeltests):
+ """ Vote tests. """
+
+ session = None
+
+ def test_init_vote(self):
+ """ Test the Vote init function. """
+ candidates = Candidatetests('test_init_candidate')
+ candidates.session = self.session
+ candidates.test_init_candidate()
+
+ obj = models.Vote( # id:1
+ election_id=1,
+ voter='toshio',
+ candidate_id=1,
+ value='3',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:2
+ election_id=1,
+ voter='toshio',
+ candidate_id=2,
+ value='3',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:3
+ election_id=1,
+ voter='toshio',
+ candidate_id=3,
+ value='3',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:4
+ election_id=1,
+ voter='ralph',
+ candidate_id=2,
+ value=2,
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Vote( # id:5
+ election_id=1,
+ voter='ralph',
+ candidate_id=3,
+ value=1,
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ def test_vote_count_with_votes(self):
+ """ Test the Candidate.vote_count function with votes in. """
+ self.test_init_vote()
+
+ obj = models.Candidate.get(self.session, 2)
+ self.assertEqual(obj.vote_count, 5)
+
+ obj = models.Candidate.by_id(self.session, 3)
+ self.assertEqual(obj.vote_count, 4)
+
+ obj = models.Candidate.get(self.session, 1)
+ self.assertEqual(obj.vote_count, 3)
+
+ def test_of_user_on_election(self):
+ """ Test the Vote.of_user_on_election function. """
+ self.test_init_vote()
+
+ obj = models.Vote.of_user_on_election(
+ self.session, 'toshio', 1, count=True)
+ self.assertEqual(obj, 3)
+
+ obj = models.Vote.of_user_on_election(
+ self.session, 'toshio', 1, count=False)
+ self.assertEqual(len(obj), 3)
+ self.assertEqual(obj[0].voter, 'toshio')
+ self.assertEqual(obj[0].candidate.name, 'Toshio')
+ self.assertEqual(obj[1].voter, 'toshio')
+ self.assertEqual(obj[1].candidate.name, 'Kevin')
+ self.assertEqual(obj[2].voter, 'toshio')
+ self.assertEqual(obj[2].candidate.name, 'Ralph')
+
+ def test_get_election_stats(self):
+ """ Test the get_election_stats function. """
+ self.test_init_vote()
+
+ obj = models.Vote.get_election_stats(self.session, 1)
+ self.assertEqual(obj['n_voters'], 2)
+ self.assertEqual(obj['n_votes'], 5)
+ self.assertEqual(
+ obj['candidate_voters'],
+ {'Toshio': 1, 'Ralph': 2, 'Kevin': 2}
+ )
+ self.assertEqual(obj['n_candidates'], 3)
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(Votetests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit 8b2b498eaf456c9c4c50576611bb9df5eb2aa863
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:27:03 2014 +0200
Add unit-tests for the Candidate object
diff --git a/tests/test_candidate.py b/tests/test_candidate.py
new file mode 100644
index 0000000..2d7b47d
--- /dev/null
+++ b/tests/test_candidate.py
@@ -0,0 +1,114 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections.model.Candidate test script
+"""
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import unittest
+import sys
+import os
+
+from datetime import time
+from datetime import timedelta
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+from fedora_elections import models
+from tests import Modeltests, TODAY
+from test_election import Electiontests
+
+
+# pylint: disable=R0904
+class Candidatetests(Modeltests):
+ """ Candidate tests. """
+
+ session = None
+
+ def test_init_candidate(self):
+ """ Test the Candidate init function. """
+ elections = Electiontests('test_init_election')
+ elections.session = self.session
+ elections.test_init_election()
+
+ obj = models.Candidate( # id:1
+ election_id=1,
+ name='Toshio',
+ url='https://fedoraproject.org/wiki/User:Toshio',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Candidate( # id:2
+ election_id=1,
+ name='Kevin',
+ url='https://fedoraproject.org/wiki/User:Kevin',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ obj = models.Candidate( # id:3
+ election_id=1,
+ name='Ralph',
+ url='https://fedoraproject.org/wiki/User:Ralph',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ def test_to_json(self):
+ """ Test the Candidate.to_json function. """
+ self.test_init_candidate()
+ obj = models.Candidate.by_id(self.session, 3)
+ self.assertEqual(
+ obj.to_json(),
+ {
+ 'name': 'Ralph',
+ 'url': 'https://fedoraproject.org/wiki/User:Ralph',
+ }
+ )
+
+ obj = models.Candidate.get(self.session, 2)
+ self.assertEqual(
+ obj.to_json(),
+ {
+ 'name': 'Kevin',
+ 'url': 'https://fedoraproject.org/wiki/User:Kevin',
+ }
+ )
+
+ def test_vote_count(self):
+ """ Test the Candidate.vote_count function. """
+ self.test_init_candidate()
+
+ obj = models.Candidate.by_id(self.session, 3)
+ self.assertEqual(obj.vote_count, 0)
+
+ obj = models.Candidate.get(self.session, 2)
+ self.assertEqual(obj.vote_count, 0)
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(Candidatetests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit 87e90f1909b00b7a44ac05f3bd721c31a9644e40
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:26:34 2014 +0200
Add unit-tests for the Election object
diff --git a/tests/test_election.py b/tests/test_election.py
new file mode 100644
index 0000000..73709d2
--- /dev/null
+++ b/tests/test_election.py
@@ -0,0 +1,248 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections.model.Election test script
+"""
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import unittest
+import sys
+import os
+
+from datetime import time
+from datetime import timedelta
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+from fedora_elections import models
+from tests import Modeltests, TODAY
+
+
+# pylint: disable=R0904
+class Electiontests(Modeltests):
+ """ Election tests. """
+
+ session = None
+
+ def test_init_election(self):
+ """ Test the Election init function. """
+ # Election finished - result open
+ obj = models.Election( # id:1
+ shortdesc='test election shortdesc',
+ alias='test_election',
+ description='test election description',
+ url='https://fedoraproject.org',
+ start_date=TODAY - timedelta(days=10),
+ end_date=TODAY - timedelta(days=8),
+ seats_elected=1,
+ embargoed=0,
+ voting_type='range',
+ candidates_are_fasusers=0,
+ fas_user='ralph',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ # Election finished - result embargoed
+ obj = models.Election( # id:2
+ shortdesc='test election 2 shortdesc',
+ alias='test_election2',
+ description='test election 2 description',
+ url='https://fedoraproject.org',
+ start_date=TODAY - timedelta(days=7),
+ end_date=TODAY - timedelta(days=5),
+ seats_elected=1,
+ embargoed=1,
+ voting_type='range',
+ candidates_are_fasusers=0,
+ fas_user='kevin',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ # Election open
+ obj = models.Election( # id:3
+ shortdesc='test election 3 shortdesc',
+ alias='test_election3',
+ description='test election 3 description',
+ url='https://fedoraproject.org',
+ start_date=TODAY - timedelta(days=2),
+ end_date=TODAY + timedelta(days=2),
+ seats_elected=1,
+ embargoed=1,
+ voting_type='range',
+ candidates_are_fasusers=0,
+ fas_user='pingou',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ # Election created but not open
+ obj = models.Election( # id:4
+ shortdesc='test election 4 shortdesc',
+ alias='test_election4',
+ description='test election 4 description',
+ url='https://fedoraproject.org',
+ start_date=TODAY + timedelta(days=3),
+ end_date=TODAY + timedelta(days=6),
+ seats_elected=1,
+ embargoed=1,
+ voting_type='range',
+ candidates_are_fasusers=0,
+ fas_user='skvidal',
+ )
+ self.session.add(obj)
+ self.session.commit()
+ self.assertNotEqual(obj, None)
+
+ def test_get_election(self):
+ """ Test the Election.get function. """
+ self.test_init_election()
+ obj = models.Election.get(self.session, 'test_election')
+ self.assertNotEqual(obj, None)
+ self.assertEqual(obj.id, 1)
+
+ def test_status_election(self):
+ """ Test the Election.status function. """
+ self.test_init_election()
+ obj = models.Election.get(self.session, 'test_election')
+ self.assertNotEqual(obj, None)
+ self.assertEqual(obj.status, 'Ended')
+
+ obj = models.Election.get(self.session, 'test_election2')
+ self.assertNotEqual(obj, None)
+ self.assertEqual(obj.status, 'Embargoed')
+
+ obj = models.Election.get(self.session, 'test_election3')
+ self.assertNotEqual(obj, None)
+ self.assertEqual(obj.status, 'In progress')
+
+ obj = models.Election.get(self.session, 'test_election4')
+ self.assertNotEqual(obj, None)
+ self.assertEqual(obj.status, 'Pending')
+
+ def test_locked_election(self):
+ """ Test the Election.locked function. """
+ self.test_init_election()
+ obj = models.Election.get(self.session, 'test_election')
+ self.assertNotEqual(obj, None)
+ self.assertTrue(obj.locked)
+
+ obj = models.Election.get(self.session, 'test_election2')
+ self.assertNotEqual(obj, None)
+ self.assertTrue(obj.locked)
+
+ obj = models.Election.get(self.session, 'test_election3')
+ self.assertNotEqual(obj, None)
+ self.assertTrue(obj.locked)
+
+ obj = models.Election.get(self.session, 'test_election4')
+ self.assertNotEqual(obj, None)
+ self.assertFalse(obj.locked)
+
+ def test_search_election(self):
+ """ Test the Election.search function. """
+ self.test_init_election()
+ obj = models.Election.search(self.session, alias='test_election')
+ self.assertNotEqual(obj, None)
+ self.assertNotEqual(obj, [])
+ self.assertEqual(len(obj), 1)
+ self.assertEqual(obj[0].shortdesc, 'test election shortdesc')
+
+ obj = models.Election.search(
+ self.session, shortdesc='test election 2 shortdesc')
+ self.assertNotEqual(obj, None)
+ self.assertNotEqual(obj, [])
+ self.assertEqual(len(obj), 1)
+ self.assertEqual(obj[0].description, 'test election 2 description')
+
+ obj = models.Election.search(
+ self.session, fas_user='skvidal')
+ self.assertNotEqual(obj, None)
+ self.assertNotEqual(obj, [])
+ self.assertEqual(len(obj), 1)
+ self.assertEqual(obj[0].description, 'test election 4 description')
+
+ obj = models.Election.search(self.session)
+ self.assertNotEqual(obj, None)
+ self.assertNotEqual(obj, [])
+ self.assertEqual(len(obj), 4)
+ self.assertEqual(obj[0].description, 'test election 4 description')
+ self.assertEqual(obj[1].description, 'test election 3 description')
+ self.assertEqual(obj[2].description, 'test election 2 description')
+ self.assertEqual(obj[3].description, 'test election description')
+
+ def test_get_older_election(self):
+ """ Test the Election.get_older_election function. """
+ self.test_init_election()
+ obj = models.Election.get_older_election(self.session, limit=TODAY)
+ self.assertNotEqual(obj, None)
+ self.assertNotEqual(obj, [])
+ self.assertEqual(len(obj), 2)
+ self.assertEqual(obj[0].shortdesc, 'test election 2 shortdesc')
+ self.assertEqual(obj[1].shortdesc, 'test election shortdesc')
+
+ def test_get_open_election(self):
+ """ Test the Election.get_open_election function. """
+ self.test_init_election()
+ obj = models.Election.get_open_election(self.session, limit=TODAY)
+ self.assertNotEqual(obj, None)
+ self.assertNotEqual(obj, [])
+ self.assertEqual(len(obj), 1)
+ self.assertEqual(obj[0].shortdesc, 'test election 3 shortdesc')
+
+ def test_get_next_election(self):
+ """ Test the Election.get_next_election function. """
+ self.test_init_election()
+ obj = models.Election.get_next_election(self.session, limit=TODAY)
+ self.assertNotEqual(obj, None)
+ self.assertNotEqual(obj, [])
+ self.assertEqual(len(obj), 1)
+ self.assertEqual(obj[0].shortdesc, 'test election 4 shortdesc')
+
+ def test_to_json(self):
+ """ Test the Election.to_json function. """
+ self.test_init_election()
+ obj = models.Election.get(self.session, 'test_election')
+ self.assertEqual(
+ obj.to_json(),
+ {
+ 'description': 'test election description',
+ 'end_date': (
+ TODAY - timedelta(days=8)).strftime('%Y-%m-%d %H:%M'),
+ 'url': 'https://fedoraproject.org',
+ 'embargoed': 0,
+ 'alias': 'test_election',
+ 'shortdesc': 'test election shortdesc',
+ 'voting_type': 'range',
+ 'start_date': (
+ TODAY - timedelta(days=10)).strftime('%Y-%m-%d %H:%M'),
+ }
+ )
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(Electiontests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit 370d447f811cf3f51ed23e046f488dcc1d84b310
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:25:29 2014 +0200
Add first code for the unit-tests
Create the basic `Modeltests` class that all the tests will inherit from
Create basic functions to be used when testing the flask application
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..59e94fe
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,145 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+
+"""
+ (c) 2014 - Copyright Pierre-Yves Chibon
+ Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
+
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+
+ fedora_elections test script
+"""
+
+__requires__ = ['SQLAlchemy >= 0.7']
+import pkg_resources
+
+import unittest
+import sys
+import os
+
+from datetime import date
+from datetime import timedelta
+from functools import wraps
+
+from contextlib import contextmanager
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import scoped_session
+
+sys.path.insert(0, os.path.join(os.path.dirname(
+ os.path.abspath(__file__)), '..'))
+
+from fedora_elections import models
+
+
+DB_PATH = 'sqlite:///:memory:'
+FAITOUT_URL = 'http://209.132.184.152/faitout/'
+try:
+ import requests
+ req = requests.get('%s/new' % FAITOUT_URL)
+ if req.status_code == 200:
+ DB_PATH = req.text
+ print 'Using faitout at: %s' % DB_PATH
+except:
+ pass
+
+
+TODAY = date.today()
+
+
+@contextmanager
+def user_set(APP, user):
+ """ Set the provided user as fas_user in the provided application."""
+
+ # Hack used to remove the before_request function set by
+ # flask.ext.fas_openid.FAS which otherwise kills our effort to set a
+ # flask.g.fas_user.
+ from flask import appcontext_pushed, g
+ APP.before_request_funcs[None] = []
+
+ def handler(sender, **kwargs):
+ g.fas_user = user
+
+ with appcontext_pushed.connected_to(handler, APP):
+ yield
+
+
+class Modeltests(unittest.TestCase):
+ """ Model tests. """
+
+ def __init__(self, method_name='runTest'):
+ """ Constructor. """
+ unittest.TestCase.__init__(self, method_name)
+ self.session = None
+
+ # pylint: disable=C0103
+ def setUp(self):
+ """ Set up the environnment, ran before every tests. """
+ self.session = models.create_tables(DB_PATH)
+
+ # pylint: disable=C0103
+ def tearDown(self):
+ """ Remove the test.db database if there is one. """
+ self.session.close()
+ if os.path.exists(DB_PATH):
+ os.unlink(DB_PATH)
+ if DB_PATH.startswith('postgres'):
+ if 'localhost' in DB_PATH:
+ models.drop_tables(DB_PATH, self.session.bind)
+ else:
+ db_name = DB_PATH.rsplit('/', 1)[1]
+ requests.get('%s/clean/%s' % (FAITOUT_URL, db_name))
+
+
+class FakeGroup(object):
+ """ Fake object used to make the FakeUser object closer to the
+ expectations.
+ """
+
+ def __init__(self, name):
+ """ Constructor.
+ :arg name: the name given to the name attribute of this object.
+ """
+ self.name = name
+ self.group_type = 'cla'
+
+
+# pylint: disable=R0903
+class FakeUser(object):
+ """ Fake user used to test the fedocallib library. """
+
+ def __init__(self, groups=[], username='username', cla_done=True):
+ """ Constructor.
+ :arg groups: list of the groups in which this fake user is
+ supposed to be.
+ """
+ if isinstance(groups, basestring):
+ groups = [groups]
+ self.groups = groups
+ self.username = username
+ self.name = username
+ self.approved_memberships = [
+ FakeGroup('packager'),
+ FakeGroup('design-team')]
+ self.dic = {}
+ self.dic['timezone'] = 'Europe/Paris'
+ self.cla_done = cla_done
+
+ def __getitem__(self, key):
+ return self.dic[key]
+
+
+if __name__ == '__main__':
+ SUITE = unittest.TestLoader().loadTestsFromTestCase(Modeltests)
+ unittest.TextTestRunner(verbosity=2).run(SUITE)
commit 0d05ea00928e73fb60ffe86b31d7020d26eba358
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:24:58 2014 +0200
Ignore a small piece of the createdb function in the unit-tests
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 579d4de..8eb511e 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -40,7 +40,7 @@ def create_tables(db_url, alembic_ini=None, debug=False):
engine = create_engine(db_url, echo=debug)
BASE.metadata.create_all(engine)
#engine.execute(collection_package_create_view(driver=engine.driver))
- if db_url.startswith('sqlite:'):
+ if db_url.startswith('sqlite:'): # pragma: no cover
## Ignore the warning about con_record
# pylint: disable=W0613
def _fk_pragma_on_connect(dbapi_con, con_record):
commit 2944a4603842136eb872e31169e50022fccb720a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:22:11 2014 +0200
Order votes by timestamp
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index e0ba52b..579d4de 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -302,6 +302,8 @@ class Vote(BASE):
cls.election_id == election_id
).filter(
cls.voter == user
+ ).order_by(
+ cls.timestamp
)
if count:
commit e007c5da1c7099d618469c01729b073db9f16e60
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:12:17 2014 +0200
Format the start and end dates in the json representation of an election
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 9e3c2ae..e0ba52b 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -105,8 +105,8 @@ class Election(BASE):
alias=self.alias,
description=self.description,
url=self.url,
- start_date=self.start_date,
- end_date=self.end_date,
+ start_date=self.start_date.strftime('%Y-%m-%d %H:%M'),
+ end_date=self.end_date.strftime('%Y-%m-%d %H:%M'),
embargoed=self.embargoed,
voting_type=self.voting_type,
)
commit dbf3e2d28a40d4135a24e1c60650b043265cc5c8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:09:33 2014 +0200
Add default datetime in the timestamp field of the Vote table
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 90054bf..9e3c2ae 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -3,6 +3,7 @@
import sqlalchemy as sa
from sqlalchemy import create_engine
+from sqlalchemy import func as safunc
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.ext.declarative import declarative_base
@@ -282,7 +283,7 @@ class Vote(BASE):
election = relationship(
'Election', backref=backref('votes', lazy='dynamic'))
voter = sa.Column(sa.Unicode(150), nullable=False)
- timestamp = sa.Column(sa.DateTime, nullable=False)
+ timestamp = sa.Column(sa.DateTime, nullable=False, default=safunc.now())
candidate_id = sa.Column(sa.Integer, sa.ForeignKey('candidates.id'),
nullable=False)
candidate = relationship(
commit 50c91f1fb2a4a6dacda9f93419642f5e6b669b73
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:08:34 2014 +0200
pep8 fixes on the controllers
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index a11898d..9873635 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -67,7 +67,7 @@ from fedora_elections import forms
def is_authenticated():
''' Return a boolean specifying if the user is authenticated or not.
'''
- return hasattr(flask.g, 'fas_user') and not flask.g.fas_user is None
+ return hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None
def is_safe_url(target):
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
index d0a6f43..a943086 100644
--- a/fedora_elections/admin.py
+++ b/fedora_elections/admin.py
@@ -33,9 +33,10 @@ from sqlalchemy.orm.exc import NoResultFound
from fedora_elections import fedmsgshim
from fedora_elections import forms
from fedora_elections import models
-from fedora_elections import APP, SESSION, is_authenticated, is_admin, \
- is_election_admin, is_safe_url, \
- safe_redirect_back
+from fedora_elections import (
+ APP, SESSION, is_authenticated, is_admin, is_election_admin,
+ is_safe_url, safe_redirect_back
+)
def election_admin_required(f):
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index b556e13..0afb02e 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -33,8 +33,10 @@ from sqlalchemy.orm.exc import NoResultFound
from fedora_elections import fedmsgshim
from fedora_elections import forms
from fedora_elections import models
-from fedora_elections import APP, SESSION, is_authenticated, is_admin,\
- is_safe_url, safe_redirect_back
+from fedora_elections import (
+ APP, SESSION, is_authenticated, is_admin, is_safe_url,
+ safe_redirect_back,
+)
def login_required(f):
@@ -76,7 +78,6 @@ def get_valid_election(election_alias, ended=False):
return election
-
@APP.route('/vote/<election_alias>', methods=['GET', 'POST'])
@login_required
def vote(election_alias):
commit 6b7e934c87169401b1fd949507139e6b8bfe20c6
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 11:08:34 2014 +0200
Make the minimal flask version be 0.10
diff --git a/requirements.txt b/requirements.txt
index ca5b050..28de0aa 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
# Used for when working from a virtualenv.
# Use this file by running "$ pip install -r requirements.txt"
-flask
+flask>=0.10
kitchen
python-fedora
Flask-wtf
commit 17dd53ea5b2e7566a2482ae8142179373a37917d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 11:05:52 2014 +0200
Remove current tests
diff --git a/tests/base.py b/tests/base.py
deleted file mode 100644
index c0e5c73..0000000
--- a/tests/base.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2012-2014 Red Hat, Inc.
-#
-# Author(s): Toshio Kuratomi <toshio(a)fedoraproject.org>
-
-import os
-
-import fedora_elections
-from fedora_elections.models import create_tables
-
-# Note: This is used in unittests that use nose's generator tests so we can't
-# subclass unittest.TestCase
-class BaseRequestTests(object):
- '''Setup application for further testing of requests'''
-
- def setUp(self):
- cur_folder = os.path.dirname(os.path.realpath(__file__))
- db_file = os.path.join(cur_folder, 'testing.sqlite')
- print '* creating %s' % db_file
- fedora_elections.APP.config['DB_URL'] = 'sqlite:///%s' % db_file
-
- self.session = create_tables(fedora_elections.APP.config['DB_URL'])
-
- fedora_elections.APP.config['TESTING'] = True
- fedora_elections.APP.SESSION = self.session
- self.app = fedora_elections.APP.test_client()
-
- def tearDown(self):
- cur_folder = os.path.dirname(os.path.realpath(__file__))
- db_file = os.path.join(cur_folder, 'testing.sqlite')
- print '* cleaning %s' % db_file
- os.unlink(db_file)
-
-
-url_subs = {'election_alias': None, 'candidate_id': None}
-
-anon_urls = (
- '/',
- '/archive',
- '/login',
- '/logout',
- '/vote/%(election_alias)s')
-
-authed_view_urls = (
- '/logout',
- '/vote/%(election_alias)s')
-
-admin_view_urls = (
- '/admin',
- '/admin/new',
- '/admin/%(election_alias)s')
-
-admin_modify_urls = (
- '/admin/new',
- '/admin/%(election_alias)s/edit',
- '/admin/%(election_alias)s/candidates/new',
- '/admin/%(election_alias)s/candidates/%(candidate_id)d/edit',
- '/admin/%(election_alias)s/candidates/%(candidate_id)d/delete')
diff --git a/tests/test_smoke.py b/tests/test_smoke.py
deleted file mode 100644
index 0fbb3d2..0000000
--- a/tests/test_smoke.py
+++ /dev/null
@@ -1,100 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2012 Red Hat, Inc.
-#
-# Author(s): Toshio Kuratomi <toshio(a)fedoraproject.org>
-
-from copy import copy
-import datetime
-from functools import partial
-
-from mock import Mock
-from nose import tools, with_setup
-
-from base import BaseRequestTests, anon_urls, authed_view_urls, \
- admin_view_urls, admin_modify_urls, url_subs
-
-import fedora_elections
-import fedora_elections.models
-
-class TestSmoke(BaseRequestTests):
- '''Basic test that we don't get 500s'''
-
- def setUp(self):
- super(TestSmoke, self).setUp()
- fedora_elections.SESSION = self.session
-
- fedora_elections.FAS.login = Mock(return_value=True)
- # Add an election
- election = fedora_elections.models.Election(
- summary='Test Election 1',
- alias='test1',
- description='lorem ipsum of course',
- url='http://fedorahosted.org/elections',
- start_date=datetime.datetime.utcnow(),
- end_date=datetime.datetime.utcnow()+datetime.timedelta(hours=1),
- fas_user='fchiulli',
- )
- self.session.add(election)
-
- # Add candidate to election
- candidate = fedora_elections.models.Candidate(
- election=election,
- name='Mr Testy',
- url='http://fedoraproject.org')
- self.session.add(candidate)
-
- self.session.commit()
-
- self.url_subs = copy(url_subs)
- self.url_subs['election_alias'] = 'test1'
- self.url_subs['candidate_id'] = candidate.id
-
- def tearDown(self):
- super(TestSmoke, self).tearDown()
-
- def setup_auth(self, username, password):
- self.setUp()
- self.app.post('/login', data=dict( username=username,
- password=password), follow_redirects=True)
-
- def teardown_auth(self):
- self.tearDown()
- return self.app.get('/logout', follow_redirects=True)
-
- def check_status_code(self, url, status_codes=[200]):
- resp = self.app.get(url % self.url_subs, follow_redirects=True)
- tools.assert_in(resp.status_code, status_codes)
-
- @with_setup(partial(setup_auth, 'user', '123'), teardown_auth)
- def check_user_status_code(self, *args, **kwargs):
- self.check_status_code(*args, **kwargs)
-
- @with_setup(partial(setup_auth, 'admin', '123'), teardown_auth)
- def check_admin_status_code(self, *args, **kwargs):
- self.check_status_code(*args, **kwargs)
-
- def test_all_url_subs(self):
- '''Testing that the testsuite is going to substitute all variables
- in the url
- '''
- for i in self.url_subs.values():
- if i is None:
- raise Exception('Smoke test must be updated for new url '
- 'variable present in base.py')
-
- def test_anon_view(self):
- for url in anon_urls:
- yield self.check_status_code, url, [200]
-
- def test_authed_view(self):
- for url in (u for u in authed_view_urls):
- yield self.check_user_status_code, url, [200]
-
- def test_admin_view(self):
- for url in (u for u in admin_view_urls):
- yield self.check_admin_status_code, url, [200]
-
- def test_admin_modify(self):
- for url in (u for u in admin_modify_urls):
- yield self.check_admin_status_code, url, [200]
commit bd2db38ad9e04f4bd1a26e833b6e88f8a4a12ff6
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 14:11:06 2014 +0200
Adjust the database mapping of candidates_are_fasusers to its type in the DB
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 4c4e300..90054bf 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -93,8 +93,8 @@ class Election(BASE):
seats_elected = sa.Column(sa.Integer, nullable=False, default=1)
embargoed = sa.Column(sa.Integer, nullable=False, default=0)
voting_type = sa.Column(sa.Unicode(100), nullable=False, default=u'range')
- candidates_are_fasusers = sa.Column(sa.Boolean, nullable=False,
- default=False)
+ candidates_are_fasusers = sa.Column(
+ sa.Integer, nullable=False, default=0)
fas_user = sa.Column(sa.Unicode(50), nullable=False)
def to_json(self):
commit 599405957ed21ed522b2f77b51807aa1f975f630
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 14:05:09 2014 +0200
Rename the column usefas to candidates_are_fasusers
diff --git a/files/update_1_to_2.sql b/files/update_1_to_2.sql
index 99da757..79bb69c 100644
--- a/files/update_1_to_2.sql
+++ b/files/update_1_to_2.sql
@@ -5,7 +5,7 @@
ALTER TABLE elections ADD COLUMN voting_type VARCHAR(100) NOT NULL DEFAULT 'range';
-ALTER TABLE elections ADD COLUMN candidates_are_fasusers BOOLEAN NOT NULL DEFAULT false;
+-- ALTER TABLE elections ADD COLUMN candidates_are_fasusers BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE elections ADD COLUMN fas_user VARCHAR(50);
@@ -15,3 +15,7 @@ ALTER TABLE elections DROP COLUMN votes_per_user;
-- ALTER TABLE elections RENAME COLUMN shortdesc TO summary;
+
+ALTER TABLE elections DROP COLUMN candidates_are_fasusers;
+
+ALTER TABLE elections RENAME COLUMN usefas TO candidates_are_fasusers;
commit 3054b9dcad3db5e936c7b3971f800279070fd1c0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:21:10 2014 +0200
Remove import of the redirect file since the file no longer exists
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index dfa5e09..a11898d 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -62,7 +62,6 @@ FAS2 = AccountSystem(
from fedora_elections import models
SESSION = models.create_session(APP.config['DB_URL'])
from fedora_elections import forms
-from fedora_elections import redirect
def is_authenticated():
commit 316505cfddfcd5742d5ca03d454471f2b25060eb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 11:04:14 2014 +0200
Make the runserver.py file executable
diff --git a/runserver.py b/runserver.py
old mode 100644
new mode 100755
commit 54f977529c0c24df4b93f3c16ae9049dd3fb4bc8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 10:47:35 2014 +0200
Give a warning information message to admins seeing an embargoed election
And format a little the warning messages (as orange)
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
index b12c6c7..b556e13 100644
--- a/fedora_elections/elections.py
+++ b/fedora_elections/elections.py
@@ -286,9 +286,11 @@ def election_results(election_alias):
else:
if is_admin(flask.g.fas_user) \
or is_election_admin(flask.g.fas_user, election.id):
+ flask.flash("You are only seeing this page because you are "
+ "an admin.", "warning")
flask.flash("The results for this election are currently "
"embargoed pending formal announcement.",
- "error")
+ "warning")
pass
else:
flask.flash(
diff --git a/fedora_elections/static/elections.css b/fedora_elections/static/elections.css
index 5322b11..8aba889 100644
--- a/fedora_elections/static/elections.css
+++ b/fedora_elections/static/elections.css
@@ -7,6 +7,10 @@
}
.warning {
+ color: orange;
+}
+
+.warning {
color: #FF8000;
}
commit 0d808399e3ca1717d8ea587ecef29bc0bf2330eb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 10:34:38 2014 +0200
Split the one controller into three
- __init__ has the main endpoints where no login is necessary
- elections has the elections related endpoints (voting, results...)
- admin has the administration related endpoints (create, edit, delete...)
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index cfe4695..dfa5e09 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -117,55 +117,24 @@ def is_election_admin(user, election_id):
return len(set(user.groups).intersection(set(admingroups))) > 0
-def login_required(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- if not is_authenticated():
- return flask.redirect(flask.url_for(
- 'auth_login', next=flask.request.url))
- return f(*args, **kwargs)
- return decorated_function
-
-
-def election_admin_required(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- if not is_authenticated():
- return flask.redirect(flask.url_for(
- 'auth_login', next=flask.request.url))
- if not is_admin(flask.g.fas_user):
- flask.abort(403)
- return f(*args, **kwargs)
- return decorated_function
-
-
-def get_valid_election(election_alias, ended=False):
- """ Return the election if it is valid (no pending, not ended).
- """
- election = models.Election.get(SESSION, alias=election_alias)
-
- if not election:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
-
- if election.status == 'Pending':
- flask.flash('Voting has not yet started, sorry.')
- return redirect.safe_redirect_back()
-
- elif not ended and election.status == 'Ended':
- flask.flash(
- 'This election is closed. You have been redirected to the '
- 'election results.')
- return flask.redirect(flask.url_for(
- 'election_results', election_alias=election.alias))
+def is_safe_url(target):
+ ref_url = urlparse(flask.request.host_url)
+ test_url = urlparse(urljoin(flask.request.host_url, target))
+ return test_url.scheme in ('http', 'https') and \
+ ref_url.netloc == test_url.netloc
- elif ended and election.status == 'In progress':
- flask.flash(
- 'Sorry but this election is in progress, you may not see its '
- 'results already.')
- return redirect.safe_redirect_back()
- return election
+def safe_redirect_back(next=None, fallback=('index', {})):
+ targets = []
+ if next:
+ targets.append(next)
+ if 'next' in flask.request.args and \
+ flask.request.args['next']:
+ targets.append(flask.request.args['next'])
+ targets.append(flask.url_for(fallback[0], **fallback[1]))
+ for target in targets:
+ if is_safe_url(target):
+ return flask.redirect(target)
@APP.context_processor
@@ -214,7 +183,7 @@ def about_election(election_alias):
if not election:
flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
+ return safe_redirect_back()
usernamemap = {}
if (election.candidates_are_fasusers):
@@ -240,7 +209,7 @@ def archived_elections():
if not elections:
flask.flash('There are no archived elections.')
- return redirect.safe_redirect_back()
+ return safe_redirect_back()
return flask.render_template(
'archive.html',
@@ -255,7 +224,7 @@ def open_elections():
if not elections:
flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
+ return safe_redirect_back()
voted = []
if is_authenticated():
@@ -273,207 +242,10 @@ def open_elections():
title='Open Elections')
-### ELECTION VIEWS #############################################
-
-(a)APP.route('/vote/<election_alias>', methods=['GET', 'POST'])
-@login_required
-def vote(election_alias):
- election = get_valid_election(election_alias)
-
- if not isinstance(election, models.Election):
- return election
-
- votes = models.Vote.of_user_on_election(
- SESSION, flask.g.fas_user.username, election.id, count=True)
-
- if votes > 0:
- flask.flash('You have already voted in the election!')
- return redirect.safe_redirect_back()
-
- if election.voting_type == 'range':
- return vote_range(election_alias)
- elif election.voting_type == 'simple':
- return vote_simple(election_alias)
- else:
- flask.flash(
- 'Unknown election voting type: %s' % election.voting_type)
- return redirect.safe_redirect_back()
-
-
-(a)APP.route('/vote_range/<election_alias>', methods=['GET', 'POST'])
-@login_required
-def vote_range(election_alias):
- election = get_valid_election(election_alias)
-
- if not isinstance(election, models.Election):
- return election
-
- if (election.voting_type == 'simple'):
- return flask.redirect(flask.url_for(
- 'vote_simple', election_alias=election_alias))
-
- votes = models.Vote.of_user_on_election(
- SESSION, flask.g.fas_user.username, election.id, count=True)
-
- if votes > 0:
- flask.flash('You have already voted in the election!')
- return redirect.safe_redirect_back()
-
- num_candidates = election.candidates.count()
-
- uvotes = {}
- for candidate in election.candidates:
- uvotes[candidate.id] = ""
- next_action = 'vote'
-
- if flask.request.method == 'POST':
- next_action = ''
- form_values = flask.request.form.values()
- if 'Submit' in form_values:
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote >= 0 and vote <= num_candidates:
- uvotes[candidate.id] = int(vote)
- else:
- flask.flash("Invalid Ballot!")
- return redirect.safe_redirect_back()
-
- except ValueError:
- flask.flash("Invalid Ballot!")
- return redirect.safe_redirect_back()
-
- for uvote in uvotes:
- new_vote = models.Vote(
- election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=uvote,
- value=uvotes[uvote]
- )
- SESSION.add(new_vote)
- SESSION.commit()
-
- flask.flash("Your vote has been recorded. Thank you!")
- return redirect.safe_redirect_back()
-
- elif 'Preview' in form_values:
- flask.flash("Please confirm your vote!")
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote > num_candidates:
- flask.flash("Invalid data.")
- uvotes[candidate.id] = 0
- next_action = 'vote'
- elif vote >= 0:
- uvotes[candidate.id] = vote
- else:
- flask.flash("Invalid data2.")
- uvotes[c.id] = 0
- next_action = 'vote'
- except ValueError:
- flask.flash("Invalid data")
-
- if (next_action != 'vote'):
- next_action = 'confirm'
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- FAS2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template(
- 'vote_range.html',
- election=election,
- num_candidates=num_candidates,
- usernamemap=usernamemap,
- voteinfo=uvotes,
- nextaction=next_action)
-
-
-(a)APP.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
-@login_required
-def vote_simple(election_alias):
- election = get_valid_election(election_alias)
-
- if not isinstance(election, models.Election):
- return election
-
- if (election.voting_type == 'range'):
- return flask.redirect(flask.url_for(
- 'vote_range', election_alias=election_alias))
-
- votes = models.Vote.of_user_on_election(
- SESSION, flask.g.fas_user.username, election.id, count=True)
-
- if (votes != 0):
- flask.flash('You have already voted in the election!')
- return redirect.safe_redirect_back()
-
- num_candidates = election.candidates.count()
- next_action = 'vote'
- candidate_id = -1
-
- if flask.request.method == 'POST':
- next_action = ''
- form_values = flask.request.form.values()
- if 'Submit' in form_values:
- candidate_id = int(flask.request.form['candidate'])
- new_vote = models.Vote(
- election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=candidate_id,
- value=1
- )
- SESSION.add(new_vote)
- SESSION.commit()
-
- flask.flash("Your vote has been recorded. Thank you!")
- return redirect.safe_redirect_back()
-
- elif 'Preview' in form_values:
- if ('candidate' in flask.request.form):
- flask.flash("Please confirm your vote!")
- candidate_id = int(flask.request.form['candidate'])
- else:
- flask.flash("Please vote for a candidate")
- next_action = 'vote'
-
- if (next_action != 'vote'):
- next_action = 'confirm'
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- FAS2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template(
- 'vote_simple.html',
- election=election,
- num_candidates=num_candidates,
- usernamemap=usernamemap,
- candidate_id=candidate_id,
- nextaction=next_action)
-
-
-### AUTH VIEWS #############################################
-
@APP.route('/login', methods=('GET', 'POST'))
def auth_login():
if flask.g.fas_user:
- return redirect.safe_redirect_back()
+ return safe_redirect_back()
next_url = None
if 'next' in flask.request.args:
if is_safe_url(flask.request.args['next']):
@@ -494,311 +266,9 @@ def auth_logout():
FAS.logout()
flask.g.fas_user = None
flask.flash('You have been logged out')
- return redirect.safe_redirect_back()
-
-
-### ELECTION ADMIN VIEWS ###################################
+ return safe_redirect_back()
-(a)APP.route('/admin/')
-@election_admin_required
-def admin_view_elections():
- elections = models.Election.search(SESSION)
-
- return flask.render_template(
- 'admin/all_elections.html',
- elections=elections)
-
-(a)APP.route('/admin/new', methods=('GET', 'POST'))
-@election_admin_required
-def admin_new_election():
- form = forms.ElectionForm()
- if form.validate_on_submit():
-
- election = models.Election(
- shortdesc=form.shortdesc.data,
- alias=form.alias.data,
- description=form.description.data,
- url=form.url.data,
- start_date=form.start_date.data,
- end_date=form.end_date.data,
- seats_elected=form.seats_elected.data,
- embargoed=int(form.embargoed.data),
- voting_type=form.voting_type.data,
- candidates_are_fasusers=form.candidates_are_fasusers.data,
- fas_user=flask.g.fas_user.username,
- )
-
- # Fix start_date and end_date to use datetime
- election.start_date = datetime.combine(election.start_date, time())
- election.end_date = datetime.combine(election.end_date,
- time(23, 59, 59))
- SESSION.add(election)
-
- admin = models.ElectionAdminGroup(
- election=election,
- group_name=APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
- )
- SESSION.add(admin)
-
- SESSION.commit()
-
- fedmsgshim.publish(
- topic="election.new",
- msg=dict(
- agent=flask.g.fas_user.username,
- election=election.to_json(),
- )
- )
- flask.flash('Election "%s" added' % election.alias)
- return flask.redirect(flask.url_for(
- 'admin_view_election', election_alias=election.alias))
- return flask.render_template(
- 'admin/election_form.html',
- form=form,
- submit_text='Create election')
-
-
-(a)APP.route('/admin/<election_alias>/')
-@election_admin_required
-def admin_view_election(election_alias):
- election = models.Election.get(SESSION, alias=election_alias)
- if not election:
- flask.abort(404)
-
- return flask.render_template(
- 'admin/view_election.html',
- election=election)
-
-
-(a)APP.route('/admin/<election_alias>/edit', methods=('GET', 'POST'))
-@election_admin_required
-def admin_edit_election(election_alias):
- election = models.Election.get(SESSION, alias=election_alias)
- if not election:
- flask.abort(404)
-
- form = forms.ElectionForm(election.id, obj=election)
- if form.validate_on_submit():
- form.embargoed.data = int(form.embargoed.data)
- form.populate_obj(election)
- SESSION.commit()
- fedmsgshim.publish(
- topic="election.edit",
- msg=dict(
- agent=flask.g.fas_user.username,
- election=election.to_json(),
- )
- )
- flask.flash('Election "%s" saved' % election.alias)
- return flask.redirect(flask.url_for(
- 'admin_view_election', election_alias=election.alias))
-
- return flask.render_template(
- 'admin/election_form.html',
- form=form,
- submit_text='Edit election')
-
-
-(a)APP.route('/admin/<election_alias>/candidates/new', methods=('GET', 'POST'))
-@election_admin_required
-def admin_add_candidate(election_alias):
- election = models.Election.get(SESSION, alias=election_alias)
- if not election:
- flask.abort(404)
-
- form = forms.CandidateForm()
- if form.validate_on_submit():
- candidate = models.Candidate(
- election=election,
- name=form.name.data,
- url=form.url.data
- )
-
- SESSION.add(candidate)
- SESSION.commit()
- flask.flash('Candidate "%s" saved' % candidate.name)
- fedmsgshim.publish(
- topic="candidate.new",
- msg=dict(
- agent=flask.g.fas_user.username,
- election=candidate.election.to_json(),
- candidate=candidate.to_json(),
- )
- )
- return flask.redirect(flask.url_for(
- 'admin_view_election', election_alias=election.alias))
-
- return flask.render_template(
- 'admin/candidate.html',
- form=form,
- submit_text='Add candidate')
-
-
-(a)APP.route('/admin/<election_alias>/candidates/new/multi',
- methods=('GET', 'POST'))
-@election_admin_required
-def admin_add_multi_candidate(election_alias):
- election = models.Election.get(SESSION, alias=election_alias)
- if not election:
- flask.abort(404)
-
- form = forms.MultiCandidateForm()
- if form.validate_on_submit():
-
- candidates_name = []
- for entry in form.candidate.data.strip().split("|"):
- candidate = entry.split("!")
- #No url
- if len(candidate) == 1:
- cand = models.Candidate(
- election=election,
- name=candidate[0])
- SESSION.add(cand)
- candidates_name.append(cand.name)
- # With url
- elif len(candidate) == 2:
- cand = models.Candidate(
- election=election,
- name=candidate[0],
- url=candidate[1])
- SESSION.add(cand)
- candidates_name.append(cand.name)
- else:
- flask.flash("There was an issue!")
- fedmsgshim.publish(
- topic="candidate.new",
- msg=dict(
- agent=flask.g.fas_user.username,
- election=cand.election.to_json(),
- candidate=cand.to_json(),
- )
- )
-
- SESSION.commit()
- flask.flash('Added %s candidates' % len(candidates_name))
- return flask.redirect(flask.url_for(
- 'admin_view_election', election_alias=election.alias))
-
- return flask.render_template(
- 'admin/candidate_multi.html',
- form=form,
- submit_text='Add candidates')
-
-
-(a)APP.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
- methods=('GET', 'POST'))
-@election_admin_required
-def admin_edit_candidate(election_alias, candidate_id):
- election = models.Election.get(SESSION, alias=election_alias)
- candidate = models.Candidate.get(SESSION, candidate_id=candidate_id)
-
- if not election or not candidate:
- flask.abort(404)
-
- if candidate.election != election:
- flask.abort(404)
-
- form = forms.CandidateForm(obj=candidate)
- if form.validate_on_submit():
- form.populate_obj(candidate)
- SESSION.commit()
- flask.flash('Candidate "%s" saved' % candidate.name)
- fedmsgshim.publish(
- topic="candidate.edit",
- msg=dict(
- agent=flask.g.fas_user.username,
- election=candidate.election.to_json(),
- candidate=candidate.to_json(),
- )
- )
- return flask.redirect(flask.url_for(
- 'admin_view_election', election_alias=election.alias))
-
- return flask.render_template(
- 'admin/candidate.html',
- form=form,
- submit_text='Edit candidate')
-
-
-(a)APP.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
- methods=('GET', 'POST'))
-@election_admin_required
-def admin_delete_candidate(election_alias, candidate_id):
- election = models.Election.get(SESSION, alias=election_alias)
- candidate = models.Candidate.get(SESSION, candidate_id=candidate_id)
-
- if not election or not candidate:
- flask.abort(404)
-
- if candidate.election != election:
- flask.abort(404)
-
- form = forms.ConfirmationForm()
- if form.validate_on_submit():
- candidate_name = candidate.name
- SESSION.delete(candidate)
- SESSION.commit()
- flask.flash('Candidate "%s" deleted' % candidate_name)
- fedmsgshim.publish(
- topic="candidate.delete",
- msg=dict(
- agent=flask.g.fas_user.username,
- election=candidate.election.to_json(),
- candidate=candidate.to_json(),
- )
- )
- return flask.redirect(flask.url_for(
- 'admin_view_election', election_alias=election.alias))
-
- return flask.render_template(
- 'admin/delete_candidate.html',
- form=form,
- candidate=candidate)
-
-
-(a)APP.route('/results/<election_alias>')
-def election_results(election_alias):
- election = get_valid_election(election_alias, ended=True)
-
- if not isinstance(election, models.Election):
- return election
-
- elif election.embargoed:
- if not hasattr(flask.g, 'fas_user') or not flask.g.fas_user:
- flask.flash("We are sorry. The results for this election"
- "cannot be viewed because they are currently "
- "embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
- else:
- if is_admin(flask.g.fas_user) \
- or is_election_admin(flask.g.fas_user, election.id):
- flask.flash("The results for this election are currently "
- "embargoed pending formal announcement.",
- "error")
- pass
- else:
- flask.flash(
- "We are sorry. The results for this election"
- "cannot be viewed because they are currently "
- "embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- FAS2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- stats = models.Vote.get_election_stats(SESSION, election.id)
-
- return flask.render_template(
- 'results.html',
- election=election,
- usernamemap=usernamemap,
- stats=stats,
- )
+# Finalize the import of other controllers
+import admin
+import elections
diff --git a/fedora_elections/admin.py b/fedora_elections/admin.py
new file mode 100644
index 0000000..d0a6f43
--- /dev/null
+++ b/fedora_elections/admin.py
@@ -0,0 +1,310 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2012 Red Hat, Inc.
+# Copyright © 2012 Ian Weller <ianweller(a)fedoraproject.org>
+# Copyright © 2012 Toshio Kuratomi <tkuratom(a)redhat.com>
+# Copyright © 2012 Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+#
+# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
+# Toshio Kuratomi <tkuratom(a)redhat.com>
+# Frank Chiulli <fchiulli(a)fedoraproject.org>
+# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
+#
+
+from datetime import datetime, time
+from functools import wraps
+
+import flask
+from sqlalchemy.orm.exc import NoResultFound
+
+from fedora_elections import fedmsgshim
+from fedora_elections import forms
+from fedora_elections import models
+from fedora_elections import APP, SESSION, is_authenticated, is_admin, \
+ is_election_admin, is_safe_url, \
+ safe_redirect_back
+
+
+def election_admin_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if not is_authenticated():
+ return flask.redirect(flask.url_for(
+ 'auth_login', next=flask.request.url))
+ if not is_admin(flask.g.fas_user):
+ flask.abort(403)
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+(a)APP.route('/admin/')
+@election_admin_required
+def admin_view_elections():
+ elections = models.Election.search(SESSION)
+
+ return flask.render_template(
+ 'admin/all_elections.html',
+ elections=elections)
+
+
+(a)APP.route('/admin/new', methods=('GET', 'POST'))
+@election_admin_required
+def admin_new_election():
+ form = forms.ElectionForm()
+ if form.validate_on_submit():
+
+ election = models.Election(
+ shortdesc=form.shortdesc.data,
+ alias=form.alias.data,
+ description=form.description.data,
+ url=form.url.data,
+ start_date=form.start_date.data,
+ end_date=form.end_date.data,
+ seats_elected=form.seats_elected.data,
+ embargoed=int(form.embargoed.data),
+ voting_type=form.voting_type.data,
+ candidates_are_fasusers=form.candidates_are_fasusers.data,
+ fas_user=flask.g.fas_user.username,
+ )
+
+ # Fix start_date and end_date to use datetime
+ election.start_date = datetime.combine(election.start_date, time())
+ election.end_date = datetime.combine(election.end_date,
+ time(23, 59, 59))
+ SESSION.add(election)
+
+ admin = models.ElectionAdminGroup(
+ election=election,
+ group_name=APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ )
+ SESSION.add(admin)
+
+ SESSION.commit()
+
+ fedmsgshim.publish(
+ topic="election.new",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=election.to_json(),
+ )
+ )
+
+ flask.flash('Election "%s" added' % election.alias)
+ fedmsgshim.publish(topic="election.new", msg=election)
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+ return flask.render_template(
+ 'admin/election_form.html',
+ form=form,
+ submit_text='Create election')
+
+
+(a)APP.route('/admin/<election_alias>/')
+@election_admin_required
+def admin_view_election(election_alias):
+ election = models.Election.get(SESSION, alias=election_alias)
+ if not election:
+ flask.abort(404)
+
+ return flask.render_template(
+ 'admin/view_election.html',
+ election=election)
+
+
+(a)APP.route('/admin/<election_alias>/edit', methods=('GET', 'POST'))
+@election_admin_required
+def admin_edit_election(election_alias):
+ election = models.Election.get(SESSION, alias=election_alias)
+ if not election:
+ flask.abort(404)
+
+ form = forms.ElectionForm(election.id, obj=election)
+ if form.validate_on_submit():
+ form.embargoed.data = int(form.embargoed.data)
+ form.populate_obj(election)
+ SESSION.commit()
+ fedmsgshim.publish(
+ topic="election.edit",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=election.to_json(),
+ )
+ )
+ flask.flash('Election "%s" saved' % election.alias)
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+
+ return flask.render_template(
+ 'admin/election_form.html',
+ form=form,
+ submit_text='Edit election')
+
+
+(a)APP.route('/admin/<election_alias>/candidates/new', methods=('GET', 'POST'))
+@election_admin_required
+def admin_add_candidate(election_alias):
+ election = models.Election.get(SESSION, alias=election_alias)
+ if not election:
+ flask.abort(404)
+
+ form = forms.CandidateForm()
+ if form.validate_on_submit():
+ candidate = models.Candidate(
+ election=election,
+ name=form.name.data,
+ url=form.url.data
+ )
+
+ SESSION.add(candidate)
+ SESSION.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ fedmsgshim.publish(
+ topic="candidate.new",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=candidate.election.to_json(),
+ candidate=candidate.to_json(),
+ )
+ )
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+
+ return flask.render_template(
+ 'admin/candidate.html',
+ form=form,
+ submit_text='Add candidate')
+
+
+(a)APP.route('/admin/<election_alias>/candidates/new/multi',
+ methods=('GET', 'POST'))
+@election_admin_required
+def admin_add_multi_candidate(election_alias):
+ election = models.Election.get(SESSION, alias=election_alias)
+ if not election:
+ flask.abort(404)
+
+ form = forms.MultiCandidateForm()
+ if form.validate_on_submit():
+
+ candidates_name = []
+ for entry in form.candidate.data.strip().split("|"):
+ candidate = entry.split("!")
+ #No url
+ if len(candidate) == 1:
+ cand = models.Candidate(
+ election=election,
+ name=candidate[0])
+ SESSION.add(cand)
+ candidates_name.append(cand.name)
+ # With url
+ elif len(candidate) == 2:
+ cand = models.Candidate(
+ election=election,
+ name=candidate[0],
+ url=candidate[1])
+ SESSION.add(cand)
+ candidates_name.append(cand.name)
+ else:
+ flask.flash("There was an issue!")
+ fedmsgshim.publish(
+ topic="candidate.new",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=cand.election.to_json(),
+ candidate=cand.to_json(),
+ )
+ )
+
+ SESSION.commit()
+ flask.flash('Added %s candidates' % len(candidates_name))
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+
+ return flask.render_template(
+ 'admin/candidate_multi.html',
+ form=form,
+ submit_text='Add candidates')
+
+
+(a)APP.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
+ methods=('GET', 'POST'))
+@election_admin_required
+def admin_edit_candidate(election_alias, candidate_id):
+ election = models.Election.get(SESSION, alias=election_alias)
+ candidate = models.Candidate.get(SESSION, candidate_id=candidate_id)
+
+ if not election or not candidate:
+ flask.abort(404)
+
+ if candidate.election != election:
+ flask.abort(404)
+
+ form = forms.CandidateForm(obj=candidate)
+ if form.validate_on_submit():
+ form.populate_obj(candidate)
+ SESSION.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ fedmsgshim.publish(
+ topic="candidate.edit",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=candidate.election.to_json(),
+ candidate=candidate.to_json(),
+ )
+ )
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+
+ return flask.render_template(
+ 'admin/candidate.html',
+ form=form,
+ submit_text='Edit candidate')
+
+
+(a)APP.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
+ methods=('GET', 'POST'))
+@election_admin_required
+def admin_delete_candidate(election_alias, candidate_id):
+ election = models.Election.get(SESSION, alias=election_alias)
+ candidate = models.Candidate.get(SESSION, candidate_id=candidate_id)
+
+ if not election or not candidate:
+ flask.abort(404)
+
+ if candidate.election != election:
+ flask.abort(404)
+
+ form = forms.ConfirmationForm()
+ if form.validate_on_submit():
+ candidate_name = candidate.name
+ SESSION.delete(candidate)
+ SESSION.commit()
+ flask.flash('Candidate "%s" deleted' % candidate_name)
+ fedmsgshim.publish(
+ topic="candidate.delete",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=candidate.election.to_json(),
+ candidate=candidate.to_json(),
+ )
+ )
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+
+ return flask.render_template(
+ 'admin/delete_candidate.html',
+ form=form,
+ candidate=candidate)
diff --git a/fedora_elections/elections.py b/fedora_elections/elections.py
new file mode 100644
index 0000000..b12c6c7
--- /dev/null
+++ b/fedora_elections/elections.py
@@ -0,0 +1,317 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2012 Red Hat, Inc.
+# Copyright © 2012 Ian Weller <ianweller(a)fedoraproject.org>
+# Copyright © 2012 Toshio Kuratomi <tkuratom(a)redhat.com>
+# Copyright © 2012 Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+#
+# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
+# Toshio Kuratomi <tkuratom(a)redhat.com>
+# Frank Chiulli <fchiulli(a)fedoraproject.org>
+# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
+#
+
+from datetime import datetime, time
+from functools import wraps
+
+import flask
+from sqlalchemy.orm.exc import NoResultFound
+
+from fedora_elections import fedmsgshim
+from fedora_elections import forms
+from fedora_elections import models
+from fedora_elections import APP, SESSION, is_authenticated, is_admin,\
+ is_safe_url, safe_redirect_back
+
+
+def login_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if not is_authenticated():
+ return flask.redirect(flask.url_for(
+ 'auth_login', next=flask.request.url))
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+def get_valid_election(election_alias, ended=False):
+ """ Return the election if it is valid (no pending, not ended).
+ """
+ election = models.Election.get(SESSION, alias=election_alias)
+
+ if not election:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return safe_redirect_back()
+
+ if election.status == 'Pending':
+ flask.flash('Voting has not yet started, sorry.')
+ return safe_redirect_back()
+
+ elif not ended and election.status == 'Ended':
+ flask.flash(
+ 'This election is closed. You have been redirected to the '
+ 'election results.')
+ return flask.redirect(flask.url_for(
+ 'election_results', election_alias=election.alias))
+
+ elif ended and election.status == 'In progress':
+ flask.flash(
+ 'Sorry but this election is in progress, you may not see its '
+ 'results already.')
+ return safe_redirect_back()
+
+ return election
+
+
+
+(a)APP.route('/vote/<election_alias>', methods=['GET', 'POST'])
+@login_required
+def vote(election_alias):
+ election = get_valid_election(election_alias)
+
+ if not isinstance(election, models.Election):
+ return election
+
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, election.id, count=True)
+
+ if votes > 0:
+ flask.flash('You have already voted in the election!')
+ return safe_redirect_back()
+
+ if election.voting_type == 'range':
+ return vote_range(election_alias)
+ elif election.voting_type == 'simple':
+ return vote_simple(election_alias)
+ else:
+ flask.flash(
+ 'Unknown election voting type: %s' % election.voting_type)
+ return safe_redirect_back()
+
+
+(a)APP.route('/vote_range/<election_alias>', methods=['GET', 'POST'])
+@login_required
+def vote_range(election_alias):
+ election = get_valid_election(election_alias)
+
+ if not isinstance(election, models.Election):
+ return election
+
+ if (election.voting_type == 'simple'):
+ return flask.redirect(flask.url_for(
+ 'vote_simple', election_alias=election_alias))
+
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, election.id, count=True)
+
+ if votes > 0:
+ flask.flash('You have already voted in the election!')
+ return safe_redirect_back()
+
+ num_candidates = election.candidates.count()
+
+ uvotes = {}
+ for candidate in election.candidates:
+ uvotes[candidate.id] = ""
+ next_action = 'vote'
+
+ if flask.request.method == 'POST':
+ next_action = ''
+ form_values = flask.request.form.values()
+ if 'Submit' in form_values:
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote >= 0 and vote <= num_candidates:
+ uvotes[candidate.id] = int(vote)
+ else:
+ flask.flash("Invalid Ballot!")
+ return safe_redirect_back()
+
+ except ValueError:
+ flask.flash("Invalid Ballot!")
+ return safe_redirect_back()
+
+ for uvote in uvotes:
+ new_vote = models.Vote(
+ election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=uvote,
+ value=uvotes[uvote]
+ )
+ SESSION.add(new_vote)
+ SESSION.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return safe_redirect_back()
+
+ elif 'Preview' in form_values:
+ flask.flash("Please confirm your vote!")
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote > num_candidates:
+ flask.flash("Invalid data.")
+ uvotes[candidate.id] = 0
+ next_action = 'vote'
+ elif vote >= 0:
+ uvotes[candidate.id] = vote
+ else:
+ flask.flash("Invalid data2.")
+ uvotes[c.id] = 0
+ next_action = 'vote'
+ except ValueError:
+ flask.flash("Invalid data")
+
+ if (next_action != 'vote'):
+ next_action = 'confirm'
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ FAS2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template(
+ 'vote_range.html',
+ election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ voteinfo=uvotes,
+ nextaction=next_action)
+
+
+(a)APP.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
+@login_required
+def vote_simple(election_alias):
+ election = get_valid_election(election_alias)
+
+ if not isinstance(election, models.Election):
+ return election
+
+ if (election.voting_type == 'range'):
+ return flask.redirect(flask.url_for(
+ 'vote_range', election_alias=election_alias))
+
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, election.id, count=True)
+
+ if (votes != 0):
+ flask.flash('You have already voted in the election!')
+ return safe_redirect_back()
+
+ num_candidates = election.candidates.count()
+ next_action = 'vote'
+ candidate_id = -1
+
+ if flask.request.method == 'POST':
+ next_action = ''
+ form_values = flask.request.form.values()
+ if 'Submit' in form_values:
+ candidate_id = int(flask.request.form['candidate'])
+ new_vote = models.Vote(
+ election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=candidate_id,
+ value=1
+ )
+ SESSION.add(new_vote)
+ SESSION.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return safe_redirect_back()
+
+ elif 'Preview' in form_values:
+ if ('candidate' in flask.request.form):
+ flask.flash("Please confirm your vote!")
+ candidate_id = int(flask.request.form['candidate'])
+ else:
+ flask.flash("Please vote for a candidate")
+ next_action = 'vote'
+
+ if (next_action != 'vote'):
+ next_action = 'confirm'
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ FAS2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template(
+ 'vote_simple.html',
+ election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ candidate_id=candidate_id,
+ nextaction=next_action)
+
+
+(a)APP.route('/results/<election_alias>')
+def election_results(election_alias):
+ election = get_valid_election(election_alias, ended=True)
+
+ if not isinstance(election, models.Election):
+ return election
+
+ elif election.embargoed:
+ if not hasattr(flask.g, 'fas_user') or not flask.g.fas_user:
+ flask.flash("We are sorry. The results for this election"
+ "cannot be viewed because they are currently "
+ "embargoed pending formal announcement.")
+ return safe_redirect_back()
+ else:
+ if is_admin(flask.g.fas_user) \
+ or is_election_admin(flask.g.fas_user, election.id):
+ flask.flash("The results for this election are currently "
+ "embargoed pending formal announcement.",
+ "error")
+ pass
+ else:
+ flask.flash(
+ "We are sorry. The results for this election"
+ "cannot be viewed because they are currently "
+ "embargoed pending formal announcement.")
+ return safe_redirect_back()
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ FAS2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ stats = models.Vote.get_election_stats(SESSION, election.id)
+
+ return flask.render_template(
+ 'results.html',
+ election=election,
+ usernamemap=usernamemap,
+ stats=stats,
+ )
commit be1e7563a203cbe1961b6cd04bbdf04f727f2469
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 10:34:20 2014 +0200
Remove the redirect.py file
diff --git a/fedora_elections/redirect.py b/fedora_elections/redirect.py
deleted file mode 100644
index e7aa43e..0000000
--- a/fedora_elections/redirect.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import flask
-
-from urlparse import urljoin, urlparse
-
-
-def is_safe_url(target):
- ref_url = urlparse(flask.request.host_url)
- test_url = urlparse(urljoin(flask.request.host_url, target))
- return test_url.scheme in ('http', 'https') and \
- ref_url.netloc == test_url.netloc
-
-
-def safe_redirect_back(next=None, fallback=('index', {})):
- targets = []
- if next:
- targets.append(next)
- if 'next' in flask.request.args and \
- flask.request.args['next']:
- targets.append(flask.request.args['next'])
- targets.append(flask.url_for(fallback[0], **fallback[1]))
- for target in targets:
- if is_safe_url(target):
- return flask.redirect(target)
commit ecb63302f29fdb1da9d89b18517e92a639a84b94
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 09:46:06 2014 +0200
Adjust the flask application for the new template paths
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 88d0e2e..cfe4695 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -199,7 +199,7 @@ def index():
voted.append(elec)
return flask.render_template(
- 'list/index.html',
+ 'index.html',
prev_elections=prev_elections,
cur_elections=cur_elections,
next_elections=next_elections,
@@ -227,7 +227,7 @@ def about_election(election_alias):
usernamemap[candidate.id] = candidate.name
return flask.render_template(
- 'election/about.html',
+ 'about.html',
election=election,
usernamemap=usernamemap)
@@ -243,7 +243,7 @@ def archived_elections():
return redirect.safe_redirect_back()
return flask.render_template(
- 'election/archive.html',
+ 'archive.html',
elections=elections)
@@ -266,7 +266,7 @@ def open_elections():
voted.append(elec)
return flask.render_template(
- 'list/index.html',
+ 'index.html',
next_elections=elections,
voted=voted,
tag='open',
@@ -389,7 +389,7 @@ def vote_range(election_alias):
usernamemap[candidate.id] = candidate.name
return flask.render_template(
- 'election/vote_range.html',
+ 'vote_range.html',
election=election,
num_candidates=num_candidates,
usernamemap=usernamemap,
@@ -460,7 +460,7 @@ def vote_simple(election_alias):
usernamemap[candidate.id] = candidate.name
return flask.render_template(
- 'election/vote_simple.html',
+ 'vote_simple.html',
election=election,
num_candidates=num_candidates,
usernamemap=usernamemap,
@@ -797,7 +797,7 @@ def election_results(election_alias):
stats = models.Vote.get_election_stats(SESSION, election.id)
return flask.render_template(
- 'election/results.html',
+ 'results.html',
election=election,
usernamemap=usernamemap,
stats=stats,
commit 1d3eb8b6d92a02a44d923a370b3a7c3d200aaaef
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 09:44:09 2014 +0200
Move the templates in the `election` folder one level up
diff --git a/fedora_elections/templates/about.html b/fedora_elections/templates/about.html
new file mode 100644
index 0000000..7a604bb
--- /dev/null
+++ b/fedora_elections/templates/about.html
@@ -0,0 +1,40 @@
+{% extends "base.html" %}
+{% block title %}Election Information{% endblock %}
+{% block body %}
+ <h1>{{ election.shortdesc }}</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ {% if election.status == 'Ended' %}
+ <a href="/results/{{ election.alias }}">Show Results!</a>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p />
+ {% endif %}
+{% endblock %}
diff --git a/fedora_elections/templates/archive.html b/fedora_elections/templates/archive.html
new file mode 100644
index 0000000..f0d539a
--- /dev/null
+++ b/fedora_elections/templates/archive.html
@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+
+{%block tag %}archives{% endblock %}
+
+{% block body %}
+ <h2>Past Elections</h2>
+ <table border="1" cellpadding="1">
+ <tr>
+ <th></th>
+ <th>Start Date (UTC)</th>
+ <th>End Date (UTC)</th>
+ <th></th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{election.description}}</td>
+ <td>{{election.start_date}}</td>
+ <td>{{election.end_date}}</td>
+ <td>
+ <a href="about/{{ election.alias }}">More information</a>
+ </td>
+ <td>
+ <a href="results/{{ election.alias }}">Results</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+{% endblock %}
diff --git a/fedora_elections/templates/election/about.html b/fedora_elections/templates/election/about.html
deleted file mode 100644
index 7a604bb..0000000
--- a/fedora_elections/templates/election/about.html
+++ /dev/null
@@ -1,40 +0,0 @@
-{% extends "base.html" %}
-{% block title %}Election Information{% endblock %}
-{% block body %}
- <h1>{{ election.shortdesc }}</h1>
- <p>{{ election.description }}</p>
- {% if election.url %}
- <p><a href="{{ election.url }}">[More Information]</a></p>
- {% endif %}
- {% if election.status == 'Ended' %}
- <a href="/results/{{ election.alias }}">Show Results!</a>
- {% endif %}
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- {% if candidate.url %}
- <small><a href="{{ candidate.url }}">[info]</a></small>
- {% endif %}
- </td>
- </tr>
- {% endfor %}
- </table>
- {% if election.legal_voters.count() != 0 %}
- <p>
- To vote in this election you must be a member of any one of the following groups:
- </p>
- <ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
- </ul>
- {% else %}
- <p />
- {% endif %}
-{% endblock %}
diff --git a/fedora_elections/templates/election/archive.html b/fedora_elections/templates/election/archive.html
deleted file mode 100644
index f0d539a..0000000
--- a/fedora_elections/templates/election/archive.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{% extends "base.html" %}
-
-{%block tag %}archives{% endblock %}
-
-{% block body %}
- <h2>Past Elections</h2>
- <table border="1" cellpadding="1">
- <tr>
- <th></th>
- <th>Start Date (UTC)</th>
- <th>End Date (UTC)</th>
- <th></th>
- <th></th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>{{election.description}}</td>
- <td>{{election.start_date}}</td>
- <td>{{election.end_date}}</td>
- <td>
- <a href="about/{{ election.alias }}">More information</a>
- </td>
- <td>
- <a href="results/{{ election.alias }}">Results</a>
- </td>
- </tr>
- {% endfor %}
- </table>
-{% endblock %}
diff --git a/fedora_elections/templates/election/results.html b/fedora_elections/templates/election/results.html
deleted file mode 100644
index cc5e849..0000000
--- a/fedora_elections/templates/election/results.html
+++ /dev/null
@@ -1,65 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- <h1>{{ election.shortdesc }} Results</h1>
- <p>{{ election.description }}</p>
- {% if election.url %}
- <p><a href="{{ election.url }}">[More Information]</a></p>
- {% endif %}
- <table border="1" cellpadding="1">
- <tr>
- <th>Candidate</th>
- <th title="Number of votes received">Votes</th>
- </tr>
- {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{candidate.name}}
- {% endif %}
- {% if candidate.url %}
- <small><a href="{{ candidate.url }}">[info]</a></small>
- {% endif %}
- </td>
- <td>{{ candidate.vote_count }}</td>
- </tr>
- {% endfor %}
- </table>
-
- <section class="stats">
- <h3>Some statistics about this election</h3>
- <table>
- <tr>
- <th>Number of voters</th>
- <td>{{ stats['n_voters'] }}</td>
- </tr>
- <tr>
- <th title="Number of votes registered, there may be more than one vote per voters">Number of votes</th>
- <td>{{ stats['n_votes'] }}</td>
- </tr>
- <tr>
- <th title="Maximum number of votes possible">Maximum of votes</th>
- <td>{{ stats['n_voters'] * stats['n_candidates'] }}</td>
- </tr>
- {% if stats['candidate_voters'] %}
- <tr>
- <th></th>
- <th>Number of voters per candidate</th>
- <th>Average votes per candidate</th>
- </tr>
- {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
- <tr>
- <th>{{ candidate.name }}</th>
- <td>{{ stats['candidate_voters'][candidate.name] }}</td>
- {% if stats['candidate_voters'][candidate.name] %}
- <td>{{ candidate.vote_count / stats['candidate_voters'][candidate.name] }}</td>
- {% else %}
- <td> - </td>
- {% endif %}
- </tr>
- {% endfor %}
- {% endif %}
- </table>
- </section>
-{% endblock %}
diff --git a/fedora_elections/templates/election/vote_range.html b/fedora_elections/templates/election/vote_range.html
deleted file mode 100644
index 71a7ee1..0000000
--- a/fedora_elections/templates/election/vote_range.html
+++ /dev/null
@@ -1,116 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-<h2>{{election.shortdesc}}</h2>
-
-<p>{{election.description}}</p>
-
-<p><a href="{{election.url}}">[More Information]</a></p>
-
-<form action="" method="post">
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- <a href="{{candidate.url}}">[Info]</a>
- </td>
- <!-- If user is logged in: -->
- {% if g.fas_user %}
- {% if nextaction == 'vote' %}
- <td>
- <select name="{{candidate.id}}">
- {% for opt in range(0, num_candidates+1) %}
- <option value={{opt}}>{{opt}}</option>
- {% endfor %}
- </select>
- </td>
- {% endif %}
- {% if nextaction == 'confirm' %}
- <td>
- {% set cid = candidate.id %}
- <input type="hidden" name="{{candidate.id}}"
- value="{{voteinfo[cid]}}" />
- {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
- {% if weightpercent >= 90 %}
- <div class="vthot">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 70 %}
- <div class="vtwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 50 %}
- <div class="vtmedwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 30 %}
- <div class="vtmedcool">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 10 %}
- <div class="vtcool">
- {{voteinfo[cid]}}
- </div>
- {% else %}
- <div class="vtcold">
- {{voteinfo[cid]}}
- </div>
- {% endif %}
- </td>
- {% endif %}
- {% endif %}
- <!-- End if user logged in -->
- </tr>
- {% endfor %}
- </table>
- {% if g.fas_user %}
- <p>
- {% if nextaction == 'confirm' %}
- <input type="submit" name="{{nextaction}}" value="Submit" />
- {% else %}
- <input type="submit" name="{{nextaction}}" value="Preview" />
- {% endif %}
- </p>
- {% endif %}
-</form>
-
-<!-- Make this conditional on whether the user is logged in and can vote in the election -->
-{% if election.legal_voters.count() != 0 %}
- <p>
- To vote in this election you must be a member of any one of the following groups:
- </p>
- <ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
- </ul>
-{% else %}
- <p />
-{% endif %}
-<div>
- <h2>Information on Range Voting</h2>
- <p>
- Fedora Project has implemented
- <a href="http://rangevoting.org/">Range Voting</a>
- for this election, in particular the "Range (score-summing, blanks
- treated as zero score, no quorum rule)" range voting system.
- </p>
- <p>
- To cast your vote in this election simply select a value between 0 and
- {{num_candidates}} with 0 as 'least or no preference' and
- {{num_candidates}} as 'highest preference'.
- </p>
- <p>
- At the end of the election, the highest ranking candidate(s) are marked
- as the winners.
- </p>
- <p>
- For more information about Range Voting, visit the
- <a href="http://rangevoting.org/">Center for Range Voting</a>.
- </p>
-</div>
-{% endblock %}
diff --git a/fedora_elections/templates/election/vote_simple.html b/fedora_elections/templates/election/vote_simple.html
deleted file mode 100644
index 0d43183..0000000
--- a/fedora_elections/templates/election/vote_simple.html
+++ /dev/null
@@ -1,53 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-<h2>{{election.shortdesc}}</h2>
-
-<p>{{election.description}}</p>
-
-<p><a href="{{election.url}}">[More Information]</a></p>
-
-<form action="" method="post">
- <table border="0" cellpadding="1">
- {% for candidate in election.candidates|sort %}
- <tr>
- <td>
- {% if candidate.id == candidate_id %}
- <input type="radio" name="candidate" value="{{ candidate.id }}" checked="checked">
- {% else %}
- <input type="radio" name="candidate" value="{{ candidate.id }}">
- {% endif %}
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- <a href="{{candidate.url}}">[Info]</a>
- </td>
- </tr>
- {% endfor %}
- </table>
- {% if g.fas_user %}
- <p>
- {% if nextaction == 'confirm' %}
- <input type="submit" name="{{nextaction}}" value="Submit" />
- {% else %}
- <input type="submit" name="{{nextaction}}" value="Preview" />
- {% endif %}
- </p>
- {% endif %}
-</form>
-
-<!-- Make this conditional on whether the user is logged in and can vote in the election -->
-{% if election.legal_voters.count() != 0 %}
- <p>
- To vote in this election you must be a member of any one of the following groups:
- </p>
- <ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
- </ul>
-{% else %}
- <p />
-{% endif %}
-{% endblock %}
diff --git a/fedora_elections/templates/results.html b/fedora_elections/templates/results.html
new file mode 100644
index 0000000..cc5e849
--- /dev/null
+++ b/fedora_elections/templates/results.html
@@ -0,0 +1,65 @@
+{% extends "base.html" %}
+{% block body %}
+ <h1>{{ election.shortdesc }} Results</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ <tr>
+ <th>Candidate</th>
+ <th title="Number of votes received">Votes</th>
+ </tr>
+ {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{candidate.name}}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ <td>{{ candidate.vote_count }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+
+ <section class="stats">
+ <h3>Some statistics about this election</h3>
+ <table>
+ <tr>
+ <th>Number of voters</th>
+ <td>{{ stats['n_voters'] }}</td>
+ </tr>
+ <tr>
+ <th title="Number of votes registered, there may be more than one vote per voters">Number of votes</th>
+ <td>{{ stats['n_votes'] }}</td>
+ </tr>
+ <tr>
+ <th title="Maximum number of votes possible">Maximum of votes</th>
+ <td>{{ stats['n_voters'] * stats['n_candidates'] }}</td>
+ </tr>
+ {% if stats['candidate_voters'] %}
+ <tr>
+ <th></th>
+ <th>Number of voters per candidate</th>
+ <th>Average votes per candidate</th>
+ </tr>
+ {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
+ <tr>
+ <th>{{ candidate.name }}</th>
+ <td>{{ stats['candidate_voters'][candidate.name] }}</td>
+ {% if stats['candidate_voters'][candidate.name] %}
+ <td>{{ candidate.vote_count / stats['candidate_voters'][candidate.name] }}</td>
+ {% else %}
+ <td> - </td>
+ {% endif %}
+ </tr>
+ {% endfor %}
+ {% endif %}
+ </table>
+ </section>
+{% endblock %}
diff --git a/fedora_elections/templates/vote_range.html b/fedora_elections/templates/vote_range.html
new file mode 100644
index 0000000..71a7ee1
--- /dev/null
+++ b/fedora_elections/templates/vote_range.html
@@ -0,0 +1,116 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{election.shortdesc}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
+<form action="" method="post">
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ <a href="{{candidate.url}}">[Info]</a>
+ </td>
+ <!-- If user is logged in: -->
+ {% if g.fas_user %}
+ {% if nextaction == 'vote' %}
+ <td>
+ <select name="{{candidate.id}}">
+ {% for opt in range(0, num_candidates+1) %}
+ <option value={{opt}}>{{opt}}</option>
+ {% endfor %}
+ </select>
+ </td>
+ {% endif %}
+ {% if nextaction == 'confirm' %}
+ <td>
+ {% set cid = candidate.id %}
+ <input type="hidden" name="{{candidate.id}}"
+ value="{{voteinfo[cid]}}" />
+ {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
+ {% if weightpercent >= 90 %}
+ <div class="vthot">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 70 %}
+ <div class="vtwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 50 %}
+ <div class="vtmedwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 30 %}
+ <div class="vtmedcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 10 %}
+ <div class="vtcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% else %}
+ <div class="vtcold">
+ {{voteinfo[cid]}}
+ </div>
+ {% endif %}
+ </td>
+ {% endif %}
+ {% endif %}
+ <!-- End if user logged in -->
+ </tr>
+ {% endfor %}
+ </table>
+ {% if g.fas_user %}
+ <p>
+ {% if nextaction == 'confirm' %}
+ <input type="submit" name="{{nextaction}}" value="Submit" />
+ {% else %}
+ <input type="submit" name="{{nextaction}}" value="Preview" />
+ {% endif %}
+ </p>
+ {% endif %}
+</form>
+
+<!-- Make this conditional on whether the user is logged in and can vote in the election -->
+{% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+{% else %}
+ <p />
+{% endif %}
+<div>
+ <h2>Information on Range Voting</h2>
+ <p>
+ Fedora Project has implemented
+ <a href="http://rangevoting.org/">Range Voting</a>
+ for this election, in particular the "Range (score-summing, blanks
+ treated as zero score, no quorum rule)" range voting system.
+ </p>
+ <p>
+ To cast your vote in this election simply select a value between 0 and
+ {{num_candidates}} with 0 as 'least or no preference' and
+ {{num_candidates}} as 'highest preference'.
+ </p>
+ <p>
+ At the end of the election, the highest ranking candidate(s) are marked
+ as the winners.
+ </p>
+ <p>
+ For more information about Range Voting, visit the
+ <a href="http://rangevoting.org/">Center for Range Voting</a>.
+ </p>
+</div>
+{% endblock %}
diff --git a/fedora_elections/templates/vote_simple.html b/fedora_elections/templates/vote_simple.html
new file mode 100644
index 0000000..0d43183
--- /dev/null
+++ b/fedora_elections/templates/vote_simple.html
@@ -0,0 +1,53 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{election.shortdesc}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
+<form action="" method="post">
+ <table border="0" cellpadding="1">
+ {% for candidate in election.candidates|sort %}
+ <tr>
+ <td>
+ {% if candidate.id == candidate_id %}
+ <input type="radio" name="candidate" value="{{ candidate.id }}" checked="checked">
+ {% else %}
+ <input type="radio" name="candidate" value="{{ candidate.id }}">
+ {% endif %}
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ <a href="{{candidate.url}}">[Info]</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% if g.fas_user %}
+ <p>
+ {% if nextaction == 'confirm' %}
+ <input type="submit" name="{{nextaction}}" value="Submit" />
+ {% else %}
+ <input type="submit" name="{{nextaction}}" value="Preview" />
+ {% endif %}
+ </p>
+ {% endif %}
+</form>
+
+<!-- Make this conditional on whether the user is logged in and can vote in the election -->
+{% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+{% else %}
+ <p />
+{% endif %}
+{% endblock %}
commit 85ef2ed4e5cf7737d20aa1e52224f67b58c2f913
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 09:43:46 2014 +0200
Move the index template one level up
diff --git a/fedora_elections/templates/index.html b/fedora_elections/templates/index.html
new file mode 100644
index 0000000..8f70756
--- /dev/null
+++ b/fedora_elections/templates/index.html
@@ -0,0 +1,70 @@
+{% extends "base.html" %}
+
+{%block tag %}{{tag}}{% endblock %}
+
+{% macro render_elections(election_list, title) -%}
+
+ <h3>{{title}}</h3>
+
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ {% for election in election_list %}
+ <tr>
+ <td>{{ election.shortdesc }}</td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ <td>
+ {% if election.status == "In progress" %}
+ {% if election not in voted %}
+ {% if election.voting_type == 'range' %}
+ <a href="{{ url_for('vote_range', election_alias=election.alias) }}">
+ {% elif election.voting_type == 'simple' %}
+ <a href="{{ url_for('vote_simple', election_alias=election.alias) }}">
+ {% endif %}
+ Vote now!
+ </a>
+ {% endif %}
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+
+{% endmacro %}
+
+{% block body %}
+ <h2>{{ title }}</h2>
+
+ {% if tag == 'index' %}
+ <p>
+ The <a href='https://fedoraproject.org'>Fedora project</a> is a community
+ driven project. It has a number of steering commities whose responsabilities
+ is to drive the project forward.
+ </p>
+ <p>
+ These committies are elected among volunteers of the community and by
+ the community members. The present application is meant to held these
+ elections.
+ </p>
+ {% endif %}
+
+ {% if cur_elections %}
+ {{ render_elections(cur_elections, 'Current elections') }}
+ {% endif %}
+
+ {% if next_elections %}
+ {{ render_elections(next_elections, 'Next %s elections' % (next_elections|length)) }}
+ {% endif %}
+
+ {% if prev_elections %}
+ {{ render_elections(prev_elections, 'Last %s elections' % (prev_elections|length)) }}
+ {% endif %}
+
+{% endblock %}
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
deleted file mode 100644
index 8f70756..0000000
--- a/fedora_elections/templates/list/index.html
+++ /dev/null
@@ -1,70 +0,0 @@
-{% extends "base.html" %}
-
-{%block tag %}{{tag}}{% endblock %}
-
-{% macro render_elections(election_list, title) -%}
-
- <h3>{{title}}</h3>
-
- <table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- <th></th>
- </tr>
- {% for election in election_list %}
- <tr>
- <td>{{ election.shortdesc }}</td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- <td>
- {% if election.status == "In progress" %}
- {% if election not in voted %}
- {% if election.voting_type == 'range' %}
- <a href="{{ url_for('vote_range', election_alias=election.alias) }}">
- {% elif election.voting_type == 'simple' %}
- <a href="{{ url_for('vote_simple', election_alias=election.alias) }}">
- {% endif %}
- Vote now!
- </a>
- {% endif %}
- {% endif %}
- </td>
- </tr>
- {% endfor %}
- </table>
-
-{% endmacro %}
-
-{% block body %}
- <h2>{{ title }}</h2>
-
- {% if tag == 'index' %}
- <p>
- The <a href='https://fedoraproject.org'>Fedora project</a> is a community
- driven project. It has a number of steering commities whose responsabilities
- is to drive the project forward.
- </p>
- <p>
- These committies are elected among volunteers of the community and by
- the community members. The present application is meant to held these
- elections.
- </p>
- {% endif %}
-
- {% if cur_elections %}
- {{ render_elections(cur_elections, 'Current elections') }}
- {% endif %}
-
- {% if next_elections %}
- {{ render_elections(next_elections, 'Next %s elections' % (next_elections|length)) }}
- {% endif %}
-
- {% if prev_elections %}
- {{ render_elections(prev_elections, 'Last %s elections' % (prev_elections|length)) }}
- {% endif %}
-
-{% endblock %}
commit 2ddfc7c5d230a9f76ed65505cf92230a5effc3e5
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 10 13:49:47 2014 +0200
Publish fedmsg messages upon adding, editing and deleting candidates
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 03e6a12..88d0e2e 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -618,6 +618,14 @@ def admin_add_candidate(election_alias):
SESSION.add(candidate)
SESSION.commit()
flask.flash('Candidate "%s" saved' % candidate.name)
+ fedmsgshim.publish(
+ topic="candidate.new",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=candidate.election.to_json(),
+ candidate=candidate.to_json(),
+ )
+ )
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
@@ -658,6 +666,14 @@ def admin_add_multi_candidate(election_alias):
candidates_name.append(cand.name)
else:
flask.flash("There was an issue!")
+ fedmsgshim.publish(
+ topic="candidate.new",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=cand.election.to_json(),
+ candidate=cand.to_json(),
+ )
+ )
SESSION.commit()
flask.flash('Added %s candidates' % len(candidates_name))
@@ -688,6 +704,14 @@ def admin_edit_candidate(election_alias, candidate_id):
form.populate_obj(candidate)
SESSION.commit()
flask.flash('Candidate "%s" saved' % candidate.name)
+ fedmsgshim.publish(
+ topic="candidate.edit",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=candidate.election.to_json(),
+ candidate=candidate.to_json(),
+ )
+ )
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
@@ -716,6 +740,14 @@ def admin_delete_candidate(election_alias, candidate_id):
SESSION.delete(candidate)
SESSION.commit()
flask.flash('Candidate "%s" deleted' % candidate_name)
+ fedmsgshim.publish(
+ topic="candidate.delete",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=candidate.election.to_json(),
+ candidate=candidate.to_json(),
+ )
+ )
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
commit da7b095d4aaca9f6219697f76c1d2c7d560ebd48
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 10 13:49:23 2014 +0200
Add method to convert a Candidate object into a json representation of it
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 0779042..4c4e300 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -236,6 +236,13 @@ class Candidate(BASE):
name = sa.Column(sa.Unicode(150), nullable=False)
url = sa.Column(sa.Unicode(250))
+ def to_json(self):
+ ''' Return a json representation of this object. '''
+ return dict(
+ name=self.name,
+ url=self.url,
+ )
+
@property
def vote_count(self):
values = [v.value for v in self.votes]
commit 9c68bc6b96c44dff47881351ccb07338164e8f0a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 10 13:28:13 2014 +0200
Fix the conversion of the election objects into a json structure for fedmsg
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 6eda2ee..03e6a12 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -547,7 +547,7 @@ def admin_new_election():
topic="election.new",
msg=dict(
agent=flask.g.fas_user.username,
- election=election.api_repr(version=1),
+ election=election.to_json(),
)
)
flask.flash('Election "%s" added' % election.alias)
@@ -587,7 +587,7 @@ def admin_edit_election(election_alias):
topic="election.edit",
msg=dict(
agent=flask.g.fas_user.username,
- election=election.api_repr(version=1),
+ election=election.to_json(),
)
)
flask.flash('Election "%s" saved' % election.alias)
commit c09dd8a10d953919a77770eaf5491f9de5b2d531
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 19:47:44 2014 +0200
Broadcast on fedmsg when an election is added or edited
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 0af7401..6eda2ee 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -543,8 +543,14 @@ def admin_new_election():
SESSION.commit()
+ fedmsgshim.publish(
+ topic="election.new",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=election.api_repr(version=1),
+ )
+ )
flask.flash('Election "%s" added' % election.alias)
- fedmsgshim.publish(topic="election.new", msg=election)
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
return flask.render_template(
@@ -577,6 +583,13 @@ def admin_edit_election(election_alias):
form.embargoed.data = int(form.embargoed.data)
form.populate_obj(election)
SESSION.commit()
+ fedmsgshim.publish(
+ topic="election.edit",
+ msg=dict(
+ agent=flask.g.fas_user.username,
+ election=election.api_repr(version=1),
+ )
+ )
flask.flash('Election "%s" saved' % election.alias)
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
commit 143ab9f1c441d798afbe16f399abc284312f4b0c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 19:47:24 2014 +0200
Add a json compatible representation of an election object
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 0c55e49..0779042 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -97,6 +97,19 @@ class Election(BASE):
default=False)
fas_user = sa.Column(sa.Unicode(50), nullable=False)
+ def to_json(self):
+ ''' Return a json representation of this object. '''
+ return dict(
+ shortdesc=self.shortdesc,
+ alias=self.alias,
+ description=self.description,
+ url=self.url,
+ start_date=self.start_date,
+ end_date=self.end_date,
+ embargoed=self.embargoed,
+ voting_type=self.voting_type,
+ )
+
@property
def status(self):
now = datetime.utcnow()
commit 769522c4193da031e4e43b3f77f0488dc2c744d2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 19:47:07 2014 +0200
Update the content of the fedmsg.d folder, simplify it
diff --git a/fedmsg.d/base.py b/fedmsg.d/base.py
deleted file mode 100644
index 581c207..0000000
--- a/fedmsg.d/base.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# This file is part of fedmsg.
-# Copyright (C) 2012 Red Hat, Inc.
-#
-# fedmsg is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# fedmsg is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with fedmsg; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-#
-# Authors: Ralph Bean <rbean(a)redhat.com>
-#
-config = dict(
- # Set this to dev if you're hacking on fedmsg or an app.
- # Set to stg or prod if running in the Fedora Infrastructure
- environment="dev",
-
- # Default is 0
- high_water_mark=0,
- io_threads=1,
-
- ## For the fedmsg-hub and fedmsg-relay. ##
-
- # We almost always want the fedmsg-hub to be sending messages with zmq as
- # opposed to amqp or stomp.
- zmq_enabled=True,
-
- # When subscribing to messages, we want to allow splats ('*') so we tell
- # the hub to not be strict when comparing messages topics to subscription
- # topics.
- zmq_strict=False,
-)
diff --git a/fedmsg.d/elections.py b/fedmsg.d/elections.py
new file mode 100644
index 0000000..385697b
--- /dev/null
+++ b/fedmsg.d/elections.py
@@ -0,0 +1,20 @@
+""" This is an example pkgdb configuration for fedmsg.
+By convention, it is normally installed as ``/etc/fedmsg.d/pkgdb.py``
+
+For Fedora Infrastructure, our own version of this file is kept in
+``puppet/modules/fedmsg/templates/fedmsg.d/``
+
+It needs to be globally available so remote consumers know how to find the
+pkgdb producer (wsgi process).
+"""
+
+import socket
+hostname = socket.gethostname().split('.')[0]
+
+config = dict(
+ endpoints={
+ "fedora_elections.%s" % hostname: [
+ "tcp://127.0.0.1:3005",
+ ],
+ },
+)
diff --git a/fedmsg.d/endpoints.py b/fedmsg.d/endpoints.py
deleted file mode 100644
index 88b37c0..0000000
--- a/fedmsg.d/endpoints.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# This file is part of fedmsg.
-# Copyright (C) 2012 Red Hat, Inc.
-#
-# fedmsg is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# fedmsg is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with fedmsg; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-#
-# Authors: Ralph Bean <rbean(a)redhat.com>
-#
-import socket
-hostname = socket.gethostname()
-
-config = dict(
- # This is a dict of possible addresses from which fedmsg can send
- # messages. fedmsg.init(...) requires that a 'name' argument be passed
- # to it which corresponds with one of the keys in this dict.
- endpoints={
- # These are here so your local box can listen to the upstream
- # infrastructure's bus. Cool, right? :)
- "fedora-infrastructure": [
- "tcp://fedoraproject.org:9940",
- "tcp://stg.fedoraproject.org:9940",
- ],
-
- # This is the output side of the relay to which all other
- # services can listen.
- "relay_outbound": ["tcp://*:4001"],
-
- # And this is what our app actually uses.
- "fedora_elections.%s" % hostname: ["tcp://*:3007"],
- },
-
- relay_inbound="tcp://127.0.0.1:2003",
-)
diff --git a/fedmsg.d/ssl.py b/fedmsg.d/ssl.py
deleted file mode 100644
index a286c77..0000000
--- a/fedmsg.d/ssl.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# This file is part of fedmsg.
-# Copyright (C) 2012 Red Hat, Inc.
-#
-# fedmsg is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# fedmsg is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with fedmsg; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-#
-# Authors: Ralph Bean <rbean(a)redhat.com>
-#
-import os
-import socket
-
-SEP = os.path.sep
-here = os.getcwd()
-hostname = socket.gethostname()
-
-config = dict(
- sign_messages=False,
- validate_signatures=False,
- ssldir="/etc/pki/fedmsg",
-
- crl_location="https://fedoraproject.org/fedmsg/crl.pem",
- crl_cache="/tmp/crl.pem",
- crl_cache_expiry=10,
-
- certnames={
- # In prod/stg, map hostname to the name of the cert in ssldir.
- # Unfortunately, we can't use socket.getfqdn()
- #"app01.stg": "app01.stg.phx2.fedoraproject.org",
- },
-)
commit c73b4cafce03571632f966b38f8e174a80ffe04e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 3 12:42:38 2014 +0200
Bump to 2.0.2
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index dcb19ff..0af7401 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -24,7 +24,7 @@
# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
-__version__ = '2.0.1'
+__version__ = '2.0.2'
import os
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index 2daab74..f3598a7 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -1,7 +1,7 @@
%define modname fedora_elections
Name: fedora-elections
-Version: 2.0.1
+Version: 2.0.2
Release: 1%{?dist}
Summary: Fedora elections application
@@ -98,6 +98,10 @@ rm -rf $RPM_BUILD_ROOT
%changelog
+* Sat May 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.0.2-1
+- Update to 2.0.2
+- Fix is_safe_url method, imports were different here
+
* Sat May 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.0.1-1
- Update to 2.0.1
- Fix redirection after login
commit 3772fd59b6d4f12af3a7697be56e0d6b79e00e79
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 3 12:41:16 2014 +0200
Fix the is_safe_url method, the import differs here
Signed-off-by: Pierre-Yves Chibon <pingou(a)pingoured.fr>
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index d12dceb..dcb19ff 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -75,9 +75,9 @@ def is_safe_url(target):
""" Checks that the target url is safe and sending to the current
website not some other malicious one.
"""
- ref_url = urlparse.urlparse(flask.request.host_url)
- test_url = urlparse.urlparse(
- urlparse.urljoin(flask.request.host_url, target))
+ ref_url = urlparse(flask.request.host_url)
+ test_url = urlparse(
+ urljoin(flask.request.host_url, target))
return test_url.scheme in ('http', 'https') and \
ref_url.netloc == test_url.netloc
commit 2c6a551a90a1c88cf5f27bfbc01328edb92befbb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 3 12:23:31 2014 +0200
Bump to 2.0.1
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 159cc65..d12dceb 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -24,7 +24,7 @@
# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
-__version__ = '2.0'
+__version__ = '2.0.1'
import os
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index 7360ab1..2daab74 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -1,7 +1,7 @@
%define modname fedora_elections
Name: fedora-elections
-Version: 2.0
+Version: 2.0.1
Release: 1%{?dist}
Summary: Fedora elections application
@@ -98,6 +98,10 @@ rm -rf $RPM_BUILD_ROOT
%changelog
+* Sat May 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.0.1-1
+- Update to 2.0.1
+- Fix redirection after login
+
* Wed Apr 09 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.0-1
- Clean up the spec with the changes made on the setup.py
- Remove instanciated a sqlite database
commit 192f38a033ff24fcfe9f1e14845a23c38d86838f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 3 12:20:09 2014 +0200
Fix redirection bug in the login mechanism
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 58fcd09..159cc65 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -71,6 +71,17 @@ def is_authenticated():
return hasattr(flask.g, 'fas_user') and not flask.g.fas_user is None
+def is_safe_url(target):
+ """ Checks that the target url is safe and sending to the current
+ website not some other malicious one.
+ """
+ ref_url = urlparse.urlparse(flask.request.host_url)
+ test_url = urlparse.urlparse(
+ urlparse.urljoin(flask.request.host_url, target))
+ return test_url.scheme in ('http', 'https') and \
+ ref_url.netloc == test_url.netloc
+
+
def is_admin(user):
''' Is the user an elections admin.
'''
@@ -465,7 +476,8 @@ def auth_login():
return redirect.safe_redirect_back()
next_url = None
if 'next' in flask.request.args:
- next_url = flask.request.args['next']
+ if is_safe_url(flask.request.args['next']):
+ next_url = flask.request.args['next']
if not next_url or next_url == flask.url_for('.auth_login'):
next_url = flask.url_for('.index')
commit eeb1d7b9599be88d1b077f825268964639a51be0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 18:25:34 2014 +0200
Update the changelog date for 2.0 and changelog content
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index 4155beb..7360ab1 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -98,9 +98,10 @@ rm -rf $RPM_BUILD_ROOT
%changelog
-* Thu Apr 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2-2
+* Wed Apr 09 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2.0-1
- Clean up the spec with the changes made on the setup.py
- Remove instanciated a sqlite database
+- Install the sql upgrade script from elections v1 to elections v2
* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 2-1
- Simple voting
commit 1b93d8ec568a8139b2dc47b219018bdc457c154b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 15:25:25 2014 +0200
Install the update_1_to_2.sql upgrade sql script to /usr/share/fedora-elections
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index 7d776ab..4155beb 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -77,6 +77,9 @@ install -m 644 files/fedora-elections.wsgi \
install -m 644 createdb.py \
$RPM_BUILD_ROOT/%{_datadir}/fedora-elections/fedora-elections_createdb.py
+install -m 644 files/update_1_to_2.sql \
+ $RPM_BUILD_ROOT/%{_datadir}/fedora-elections/update_1_to_2.sql
+
%clean
commit 533a6de95bcc6134fa65b5d5fe46d57380b285e8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 15:22:20 2014 +0200
When retrieving vote counts, only consider votes above 0
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index e38fcc6..0c55e49 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -309,6 +309,8 @@ class Vote(BASE):
cls
).filter(
cls.election_id == election_id
+ ).filter(
+ cls.value > 0
).count()
stats['n_votes'] = n_votes
@@ -328,6 +330,8 @@ class Vote(BASE):
cls.election_id == election_id
).filter(
cls.candidate_id == cand.id
+ ).filter(
+ cls.value > 0
).count()
candidate_voters[cand.name] = n_voters
commit 1edb70205643928888dd5b4988dcd1d7594dce15
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 15:10:24 2014 +0200
Little css fix to have the same padding on each side of a table cell
diff --git a/fedora_elections/static/koji.css b/fedora_elections/static/koji.css
index 2636e83..ab7331a 100644
--- a/fedora_elections/static/koji.css
+++ b/fedora_elections/static/koji.css
@@ -191,6 +191,7 @@ th {
td
{
padding-left:15px;
+ padding-right:15px;
}
th, td {
commit 3a3e3ba28cd273bf07f772c372e4fd6b1aa29f3e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:51:12 2014 +0200
Adjust the `Vote now!` links in the open elections tab
Now the `Vote now!` links are only shown if the user has not already voted
on the elections and if logged in, otherwise the links are shown
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index c71ad76..58fcd09 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -246,9 +246,18 @@ def open_elections():
flask.flash('There are no open elections.')
return redirect.safe_redirect_back()
+ voted = []
+ if is_authenticated():
+ for elec in elections:
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, elec.id, count=True)
+ if votes > 0:
+ voted.append(elec)
+
return flask.render_template(
'list/index.html',
next_elections=elections,
+ voted=voted,
tag='open',
title='Open Elections')
commit d6fad9262aabe1adfd420d7fb5813123c3d1f273
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:48:16 2014 +0200
List all the elections to the admins
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 0360c63..c71ad76 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -481,8 +481,7 @@ def auth_logout():
@APP.route('/admin/')
@election_admin_required
def admin_view_elections():
- elections = models.Election.search(
- SESSION, fas_user=flask.g.fas_user.username)
+ elections = models.Election.search(SESSION)
return flask.render_template(
'admin/all_elections.html',
commit be7947465ad23fb3e7636f87a6399e75c3f84e45
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:47:39 2014 +0200
Order the results returned by elections.search() by election date (most recent first)
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index bed6cb2..e38fcc6 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -131,6 +131,8 @@ class Election(BASE):
if fas_user is not None:
query = query.filter(cls.fas_user == fas_user)
+ query = query.order_by(sa.desc(cls.start_date))
+
return query.all()
@classmethod
commit 5b840c68cfbb3c4a6bc674ee68089f6a6ba7e79e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:34:07 2014 +0200
If the user is authenticated, only show election that the user can vote on
If the user is authenticated, retrieve the list of elections the user has
already voted on, this list is then used to only show the link `Vote now!`
on elections the user has not already voted on
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index f53bf4d..0360c63 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -179,11 +179,20 @@ def index():
cur_elections = models.Election.get_open_election(SESSION, now)
next_elections = models.Election.get_next_election(SESSION, now)[:3]
+ voted = []
+ if is_authenticated():
+ for elec in cur_elections:
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, elec.id, count=True)
+ if votes > 0:
+ voted.append(elec)
+
return flask.render_template(
'list/index.html',
prev_elections=prev_elections,
cur_elections=cur_elections,
next_elections=next_elections,
+ voted=voted,
tag='index',
title="Elections")
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 926736a..8f70756 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -22,7 +22,7 @@
<td>{{ election.status }}</td>
<td>
{% if election.status == "In progress" %}
- {% if g.fas_user %}
+ {% if election not in voted %}
{% if election.voting_type == 'range' %}
<a href="{{ url_for('vote_range', election_alias=election.alias) }}">
{% elif election.voting_type == 'simple' %}
commit 51c409c54fccecf111e86db4ccb6043cd95d1894
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:25:20 2014 +0200
Set flask.g.fas_user to None upon logout
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 0a5c7ac..f53bf4d 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -462,6 +462,7 @@ def auth_login():
def auth_logout():
if hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None:
FAS.logout()
+ flask.g.fas_user = None
flask.flash('You have been logged out')
return redirect.safe_redirect_back()
commit 8764fcf1614bff00e4609dea8b132232fd674970
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:24:47 2014 +0200
Add a dedicated is_authenticated method and use it in the decorators
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 5ce0252..0a5c7ac 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -65,6 +65,12 @@ from fedora_elections import forms
from fedora_elections import redirect
+def is_authenticated():
+ ''' Return a boolean specifying if the user is authenticated or not.
+ '''
+ return hasattr(flask.g, 'fas_user') and not flask.g.fas_user is None
+
+
def is_admin(user):
''' Is the user an elections admin.
'''
@@ -103,7 +109,7 @@ def is_election_admin(user, election_id):
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
- if not hasattr(flask.g, 'fas_user') or flask.g.fas_user is None:
+ if not is_authenticated():
return flask.redirect(flask.url_for(
'auth_login', next=flask.request.url))
return f(*args, **kwargs)
@@ -113,7 +119,7 @@ def login_required(f):
def election_admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
- if not hasattr(flask.g, 'fas_user') or flask.g.fas_user is None:
+ if not is_authenticated():
return flask.redirect(flask.url_for(
'auth_login', next=flask.request.url))
if not is_admin(flask.g.fas_user):
commit 731466cdc31d9faa6f78acac93d878ba957116bd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 09:55:33 2014 +0200
Add sql upgrade script, allowing to convert the database from elections v1 to v2
diff --git a/files/update_1_to_2.sql b/files/update_1_to_2.sql
new file mode 100644
index 0000000..99da757
--- /dev/null
+++ b/files/update_1_to_2.sql
@@ -0,0 +1,17 @@
+-- This script is used to convert an elections database from the version 0.x
+-- to the version 2.x.
+-- There was some changes made to the model during the rewrite to flask (v2.x)
+-- and this allows to convert from v1 to v2.
+
+ALTER TABLE elections ADD COLUMN voting_type VARCHAR(100) NOT NULL DEFAULT 'range';
+
+ALTER TABLE elections ADD COLUMN candidates_are_fasusers BOOLEAN NOT NULL DEFAULT false;
+
+ALTER TABLE elections ADD COLUMN fas_user VARCHAR(50);
+
+ALTER TABLE votes RENAME COLUMN weight TO value;
+
+ALTER TABLE elections DROP COLUMN votes_per_user;
+
+
+-- ALTER TABLE elections RENAME COLUMN shortdesc TO summary;
commit 57a0d99c36c1202664936d628dbeaadcbc71af06
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 09:50:10 2014 +0200
Fix ordering of the imports
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 790b698..5ce0252 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -28,20 +28,19 @@ __version__ = '2.0'
import os
+from datetime import datetime, time
+from functools import wraps
+from urlparse import urlparse, urljoin
+
import flask
-from flask.ext.fas_openid import FAS
from fedora.client import AuthError, AppError
from fedora.client.fas2 import AccountSystem
-
+from flask.ext.fas_openid import FAS
from sqlalchemy.orm.exc import NoResultFound
import fedmsgshim
-from datetime import datetime, time
-from functools import wraps
-from urlparse import urlparse, urljoin
-
APP = flask.Flask(__name__)
APP.config.from_object('fedora_elections.default_config')
if 'FEDORA_ELECTIONS_CONFIG' in os.environ:
commit cd27da7955b3052930e137500b4d2c2d0a8befd1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 09:48:57 2014 +0200
Fix import the configuration file specified by the environment variable
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 57fd4fb..790b698 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -26,6 +26,8 @@
__version__ = '2.0'
+import os
+
import flask
from flask.ext.fas_openid import FAS
@@ -42,8 +44,7 @@ from urlparse import urlparse, urljoin
APP = flask.Flask(__name__)
APP.config.from_object('fedora_elections.default_config')
-if APP.config.get('FEDORA_ELECTIONS_CONFIG') \
- and os.path.exists(APP.config['FEDORA_ELECTIONS_CONFIG']):
+if 'FEDORA_ELECTIONS_CONFIG' in os.environ:
APP.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
# set up FAS
commit 656536fbcacd0a2ddab9a5b4dd75d93b3d0162e7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 09:33:56 2014 +0200
pep8 fixes
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 0740b91..57fd4fb 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -54,7 +54,7 @@ FAS2 = AccountSystem(
APP.config['FAS_BASE_URL'],
username=APP.config['FAS_USERNAME'],
password=APP.config['FAS_PASSWORD'],
- insecure= not APP.config['FAS_CHECK_CERT']
+ insecure=not APP.config['FAS_CHECK_CERT']
)
@@ -578,7 +578,8 @@ def admin_add_candidate(election_alias):
submit_text='Add candidate')
-(a)APP.route('/admin/<election_alias>/candidates/new/multi', methods=('GET', 'POST'))
+(a)APP.route('/admin/<election_alias>/candidates/new/multi',
+ methods=('GET', 'POST'))
@election_admin_required
def admin_add_multi_candidate(election_alias):
election = models.Election.get(SESSION, alias=election_alias)
commit f41244c6b6cfe43a59b6688f67697131edea9d76
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 09:31:55 2014 +0200
When importing multiple candidates flash the number of candidates imported
It is simpler to check if all the candidates were imported correctly and
easier to read.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 3300648..0740b91 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -610,7 +610,7 @@ def admin_add_multi_candidate(election_alias):
flask.flash("There was an issue!")
SESSION.commit()
- flask.flash('Added candidates: "%s"' % ', '.join(candidates_name))
+ flask.flash('Added %s candidates' % len(candidates_name))
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
commit e738a6dc8cfcd76bafb5a6933393228ee5fa0737
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:53:53 2014 +0200
Cover case where there has been 0 votes, thus fixing the division by 0
diff --git a/fedora_elections/templates/election/results.html b/fedora_elections/templates/election/results.html
index 329f965..cc5e849 100644
--- a/fedora_elections/templates/election/results.html
+++ b/fedora_elections/templates/election/results.html
@@ -52,7 +52,11 @@
<tr>
<th>{{ candidate.name }}</th>
<td>{{ stats['candidate_voters'][candidate.name] }}</td>
+ {% if stats['candidate_voters'][candidate.name] %}
<td>{{ candidate.vote_count / stats['candidate_voters'][candidate.name] }}</td>
+ {% else %}
+ <td> - </td>
+ {% endif %}
</tr>
{% endfor %}
{% endif %}
commit f55770ae5904554f9d7e21d5844db4f0cf2daf8a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:53:16 2014 +0200
Give the total number of candidates in the view_election table
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index 5302baf..3c63681 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -18,6 +18,7 @@
</ul>
<h3>Candidates</h3>
{% if election.candidates.all() %}
+ <p>{{ election.candidates.count() }} candidates found</p>
<ul>
{% for candidate in election.candidates.all() %}
<li>{{ candidate.name }}
commit 67a7279ef1de2aaa7252b72f726e2c648ee4a340
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:53:01 2014 +0200
Fix the default title in the base master
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
index 4dc54bb..9ec43ff 100644
--- a/fedora_elections/templates/base.html
+++ b/fedora_elections/templates/base.html
@@ -3,7 +3,7 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
- <title>{% block title %}Fedora supplementary wallpapers{% endblock %}</title>
+ <title>{% block title %}Fedora elections{% endblock %}</title>
{%block head %}{% endblock %}
<link rel="shortcut icon" type="image/vnd.microsoft.icon"
href="//fedoraproject.org/static/images/favicon.ico"/>
commit 422a0f137b9c36bd51319752605234f3cba25fda
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:52:32 2014 +0200
Fix the order_by date to order in the dates in descending order
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index fbc60dc..bed6cb2 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -150,7 +150,7 @@ class Election(BASE):
).filter(
cls.end_date < limit
).order_by(
- cls.start_date
+ sa.desc(cls.start_date)
)
return query.all()
@@ -167,7 +167,7 @@ class Election(BASE):
).filter(
cls.end_date >= limit
).order_by(
- cls.start_date
+ sa.desc(cls.start_date)
)
return query.all()
@@ -182,7 +182,7 @@ class Election(BASE):
).filter(
cls.start_date > limit
).order_by(
- cls.start_date
+ sa.desc(cls.start_date)
)
return query.all()
commit 07a125c3b0d33ed59b8bf85d17e9622fc0385e37
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:50:25 2014 +0200
Change the __tablename__ from electionadmingroups to electionadmins
electionadmins is the name of the table in elections 0.x, so let's be
consistent with it instead of requiring a rename of the table on the
database
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index ec9b296..fbc60dc 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -189,7 +189,7 @@ class Election(BASE):
class ElectionAdminGroup(BASE):
- __tablename__ = 'electionadmingroups'
+ __tablename__ = 'electionadmins'
id = sa.Column(sa.Integer, primary_key=True)
election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
commit acd0939510d22f776f26d9b84353b0f7b594b823
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:48:22 2014 +0200
Remove the `role_required` field in the electionadmingroups table
This field does not exist in elections 0.x and we do not have the
information to enforce the check anyway
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index f0d3484..3300648 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -502,7 +502,7 @@ def admin_new_election():
admin = models.ElectionAdminGroup(
election=election,
group_name=APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
- role_required=u'user')
+ )
SESSION.add(admin)
SESSION.commit()
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 528ea2d..ec9b296 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -197,8 +197,6 @@ class ElectionAdminGroup(BASE):
election = relationship(
'Election', backref=backref('admin_groups', lazy='dynamic'))
group_name = sa.Column(sa.Unicode(150), nullable=False)
- role_required = sa.Column(sa.Enum(u'user', u'sponsor', u'administrator'),
- nullable=False)
@classmethod
def by_election_id(cls, session, election_id):
commit 437ab222533e35c67824af34bb33054d0a7696a1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:43:27 2014 +0200
Change the embargoed field from Boolean to Integer
The embargoed field is an Integer field in the elections 0.x database
so revert elections 2.x to the same field format
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index efb5930..f0d3484 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -487,7 +487,7 @@ def admin_new_election():
start_date=form.start_date.data,
end_date=form.end_date.data,
seats_elected=form.seats_elected.data,
- embargoed=form.embargoed.data,
+ embargoed=int(form.embargoed.data),
voting_type=form.voting_type.data,
candidates_are_fasusers=form.candidates_are_fasusers.data,
fas_user=flask.g.fas_user.username,
@@ -538,6 +538,7 @@ def admin_edit_election(election_alias):
form = forms.ElectionForm(election.id, obj=election)
if form.validate_on_submit():
+ form.embargoed.data = int(form.embargoed.data)
form.populate_obj(election)
SESSION.commit()
flask.flash('Election "%s" saved' % election.alias)
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index ccfa1b2..528ea2d 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -91,7 +91,7 @@ class Election(BASE):
start_date = sa.Column(sa.DateTime, nullable=False)
end_date = sa.Column(sa.DateTime, nullable=False)
seats_elected = sa.Column(sa.Integer, nullable=False, default=1)
- embargoed = sa.Column(sa.Boolean, nullable=False, default=True)
+ embargoed = sa.Column(sa.Integer, nullable=False, default=0)
voting_type = sa.Column(sa.Unicode(100), nullable=False, default=u'range')
candidates_are_fasusers = sa.Column(sa.Boolean, nullable=False,
default=False)
commit 7d6c1bc616c680e1d33780c211289b67b2ef00a1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:41:05 2014 +0200
Rename the number_elected back to seats_elected in the elections table
The field number_elected was named seats_elected on elections 0.x so
let's rename it back to it to avoid changes when updating.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 3b11a51..efb5930 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -486,7 +486,7 @@ def admin_new_election():
url=form.url.data,
start_date=form.start_date.data,
end_date=form.end_date.data,
- number_elected=form.number_elected.data,
+ seats_elected=form.seats_elected.data,
embargoed=form.embargoed.data,
voting_type=form.voting_type.data,
candidates_are_fasusers=form.candidates_are_fasusers.data,
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 369a663..43b228f 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -45,7 +45,7 @@ class ElectionForm(wtf.Form):
'End date<span class="error">*</span>', [
wtforms.validators.Required()])
- number_elected = wtforms.IntegerField(
+ seats_elected = wtforms.IntegerField(
'Number elected<span class="error">*</span>', [
wtforms.validators.Required(),
wtforms.validators.NumberRange(min=1)],
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index cb272e2..ccfa1b2 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -90,7 +90,7 @@ class Election(BASE):
url = sa.Column(sa.Unicode(250), nullable=False)
start_date = sa.Column(sa.DateTime, nullable=False)
end_date = sa.Column(sa.DateTime, nullable=False)
- number_elected = sa.Column(sa.Integer, nullable=False, default=1)
+ seats_elected = sa.Column(sa.Integer, nullable=False, default=1)
embargoed = sa.Column(sa.Boolean, nullable=False, default=True)
voting_type = sa.Column(sa.Unicode(100), nullable=False, default=u'range')
candidates_are_fasusers = sa.Column(sa.Boolean, nullable=False,
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index c5bf01d..95eb06b 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -18,7 +18,7 @@
{{ render_field_in_row(form.url) }}
{{ render_field_in_row(form.start_date, " at 00:00:00 UTC") }}
{{ render_field_in_row(form.end_date, " at 23:59:59 UTC") }}
- {{ render_field_in_row(form.number_elected) }}
+ {{ render_field_in_row(form.seats_elected) }}
{{ render_field_in_row(form.candidates_are_fasusers) }}
{{ render_field_in_row(form.embargoed) }}
</table>
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index fe95c6a..5302baf 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -13,7 +13,7 @@
<ul>
<li>Start date: {{ election.start_date }}</li>
<li>End date: {{ election.end_date }}</li>
- <li>Number elected: {{ election.number_elected }}</li>
+ <li>Number elected: {{ election.seats_elected }}</li>
<li>Election type: {{ election.voting_type }}</li>
</ul>
<h3>Candidates</h3>
commit 8de0d1323ee47755b2965e21515880c9537503f0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:37:59 2014 +0200
Remove the frontpage field in the elections table
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index d42f82b..3b11a51 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -488,7 +488,6 @@ def admin_new_election():
end_date=form.end_date.data,
number_elected=form.number_elected.data,
embargoed=form.embargoed.data,
- frontpage=True,
voting_type=form.voting_type.data,
candidates_are_fasusers=form.candidates_are_fasusers.data,
fas_user=flask.g.fas_user.username,
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 57a5178..cb272e2 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -92,7 +92,6 @@ class Election(BASE):
end_date = sa.Column(sa.DateTime, nullable=False)
number_elected = sa.Column(sa.Integer, nullable=False, default=1)
embargoed = sa.Column(sa.Boolean, nullable=False, default=True)
- frontpage = sa.Column(sa.Boolean, nullable=False, default=False)
voting_type = sa.Column(sa.Unicode(100), nullable=False, default=u'range')
candidates_are_fasusers = sa.Column(sa.Boolean, nullable=False,
default=False)
@@ -117,15 +116,12 @@ class Election(BASE):
return datetime.now() >= self.start_date
@classmethod
- def search(cls, session, frontpage=None, alias=None, shortdesc=None,
+ def search(cls, session, alias=None, shortdesc=None,
fas_user=None):
""" Search the election and filter based on the arguments passed.
"""
query = session.query(cls)
- if frontpage is not None:
- query = query.filter(cls.frontpage == frontpage)
-
if alias is not None:
query = query.filter(cls.alias == alias)
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index 025b13e..fe95c6a 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -11,7 +11,6 @@
</a>
</p>
<ul>
- <li>On front page: {{ election.frontpage }}</li>
<li>Start date: {{ election.start_date }}</li>
<li>End date: {{ election.end_date }}</li>
<li>Number elected: {{ election.number_elected }}</li>
commit 5ec273f16faed4ba197f8a91a28fd423f6320147
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:25:34 2014 +0200
Rename the field `election.summary` into `election.shortdesc`
shortdesc is the field used in the elections 0.x, let's keep using it to
reduce database change when updating to 2.x
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index be60fc7..d42f82b 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -480,7 +480,7 @@ def admin_new_election():
if form.validate_on_submit():
election = models.Election(
- summary=form.summary.data,
+ shortdesc=form.shortdesc.data,
alias=form.alias.data,
description=form.description.data,
url=form.url.data,
@@ -508,7 +508,7 @@ def admin_new_election():
SESSION.commit()
- flask.flash('Election "%s" added' % election.summary)
+ flask.flash('Election "%s" added' % election.alias)
fedmsgshim.publish(topic="election.new", msg=election)
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
@@ -541,7 +541,7 @@ def admin_edit_election(election_alias):
if form.validate_on_submit():
form.populate_obj(election)
SESSION.commit()
- flask.flash('Election "%s" saved' % election.summary)
+ flask.flash('Election "%s" saved' % election.alias)
return flask.redirect(flask.url_for(
'admin_view_election', election_alias=election.alias))
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 7220374..369a663 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -6,7 +6,7 @@ from fedora_elections.models import Election
class ElectionForm(wtf.Form):
- summary = wtforms.TextField(
+ shortdesc = wtforms.TextField(
'Summary<span class="error">*</span>', [
wtforms.validators.Required(),
wtforms.validators.Length(max=150)])
@@ -61,7 +61,7 @@ class ElectionForm(wtf.Form):
form._election_id = election_id
def validate_summary(form, field):
- check = Election.search(SESSION, summary=form.summary.data)
+ check = Election.search(SESSION, shortdesc=form.shortdesc.data)
if check:
if not (form._election_id and form._election_id == check[0].id):
raise wtforms.ValidationError(
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index e482dec..57a5178 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -84,7 +84,7 @@ class Election(BASE):
__tablename__ = 'elections'
id = sa.Column(sa.Integer, primary_key=True)
- summary = sa.Column(sa.Unicode(150), unique=True, nullable=False)
+ shortdesc = sa.Column(sa.Unicode(150), unique=True, nullable=False)
alias = sa.Column(sa.Unicode(100), unique=True, nullable=False)
description = sa.Column(sa.UnicodeText, nullable=False)
url = sa.Column(sa.Unicode(250), nullable=False)
@@ -117,7 +117,7 @@ class Election(BASE):
return datetime.now() >= self.start_date
@classmethod
- def search(cls, session, frontpage=None, alias=None, summary=None,
+ def search(cls, session, frontpage=None, alias=None, shortdesc=None,
fas_user=None):
""" Search the election and filter based on the arguments passed.
"""
@@ -129,8 +129,8 @@ class Election(BASE):
if alias is not None:
query = query.filter(cls.alias == alias)
- if summary is not None:
- query = query.filter(cls.summary == summary)
+ if shortdesc is not None:
+ query = query.filter(cls.shortdesc == shortdesc)
if fas_user is not None:
query = query.filter(cls.fas_user == fas_user)
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index f27d9af..c19c831 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -27,7 +27,7 @@
<td>
<a href="{{ url_for('admin_view_election',
election_alias=election.alias) }}">
- {{ election.summary }}
+ {{ election.shortdesc }}
</a>
</td>
<td>{{ election.start_date }}</td>
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index 9488276..c5bf01d 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -11,8 +11,8 @@
<form action="" method="post">
<fieldset>
<table>
- {{ render_field_in_row(form.summary) }}
{{ render_field_in_row(form.alias) }}
+ {{ render_field_in_row(form.shortdesc) }}
{{ render_field_in_row(form.description) }}
{{ render_field_in_row(form.voting_type) }}
{{ render_field_in_row(form.url) }}
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index a36e019..025b13e 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -3,7 +3,7 @@
{%block tag %}admin{% endblock %}
{% block body %}
- <h2>{{ election.summary }}</h2>
+ <h2>{{ election.shortdesc }}</h2>
<p>{{ election.description }}</p>
<p>
<a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">
diff --git a/fedora_elections/templates/election/about.html b/fedora_elections/templates/election/about.html
index c7bf0ec..7a604bb 100644
--- a/fedora_elections/templates/election/about.html
+++ b/fedora_elections/templates/election/about.html
@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block title %}Election Information{% endblock %}
{% block body %}
- <h1>{{ election.summary }}</h1>
+ <h1>{{ election.shortdesc }}</h1>
<p>{{ election.description }}</p>
{% if election.url %}
<p><a href="{{ election.url }}">[More Information]</a></p>
diff --git a/fedora_elections/templates/election/results.html b/fedora_elections/templates/election/results.html
index 9328b1f..329f965 100644
--- a/fedora_elections/templates/election/results.html
+++ b/fedora_elections/templates/election/results.html
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block body %}
- <h1>{{ election.summary }} Results</h1>
+ <h1>{{ election.shortdesc }} Results</h1>
<p>{{ election.description }}</p>
{% if election.url %}
<p><a href="{{ election.url }}">[More Information]</a></p>
diff --git a/fedora_elections/templates/election/vote_range.html b/fedora_elections/templates/election/vote_range.html
index e3b2e84..71a7ee1 100644
--- a/fedora_elections/templates/election/vote_range.html
+++ b/fedora_elections/templates/election/vote_range.html
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block body %}
-<h2>{{election.summary}}</h2>
+<h2>{{election.shortdesc}}</h2>
<p>{{election.description}}</p>
diff --git a/fedora_elections/templates/election/vote_simple.html b/fedora_elections/templates/election/vote_simple.html
index 17434a1..0d43183 100644
--- a/fedora_elections/templates/election/vote_simple.html
+++ b/fedora_elections/templates/election/vote_simple.html
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block body %}
-<h2>{{election.summary}}</h2>
+<h2>{{election.shortdesc}}</h2>
<p>{{election.description}}</p>
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index ec0d4ca..926736a 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -16,7 +16,7 @@
</tr>
{% for election in election_list %}
<tr>
- <td>{{ election.summary }}</td>
+ <td>{{ election.shortdesc }}</td>
<td>{{ election.start_date }}</td>
<td>{{ election.end_date }}</td>
<td>{{ election.status }}</td>
commit a8e9f20ee8dc8f333f24caca4b05252c237fe185
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Apr 8 17:50:08 2014 +0200
The first release will need to be 2.0
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 3350cb4..be60fc7 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -24,7 +24,7 @@
# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
-__version__ = '0.2.0'
+__version__ = '2.0'
import flask
from flask.ext.fas_openid import FAS
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index fe10483..7d776ab 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -1,7 +1,7 @@
%define modname fedora_elections
Name: fedora-elections
-Version: 0.2
+Version: 2.0
Release: 1%{?dist}
Summary: Fedora elections application
@@ -95,12 +95,12 @@ rm -rf $RPM_BUILD_ROOT
%changelog
-* Thu Apr 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 0.2-2
+* Thu Apr 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 2-2
- Clean up the spec with the changes made on the setup.py
- Remove instanciated a sqlite database
-* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.2-1
+* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 2-1
- Simple voting
-* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1-1
+* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 2-0
- Creation
commit ed08cc38293fafa04673645b7e69a86a84e74ada
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Apr 8 17:49:16 2014 +0200
Include /usr/share/fedora-elections/ in the spec file
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index e0b4461..fe10483 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -91,6 +91,7 @@ rm -rf $RPM_BUILD_ROOT
%config(noreplace) %{_sysconfdir}/httpd/conf.d/%{name}.conf
%{python_sitelib}/%{modname}/
%{python_sitelib}/%{modname}*.egg-info/
+%{_datadir}/%{name}
%changelog
commit ed0a76456479b2d5df92cd869bc2ab90ba32308c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Apr 8 17:48:50 2014 +0200
Adjust the spec file to correspond to the configuration file
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index 08c019c..e0b4461 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -65,7 +65,7 @@ install -m 644 files/fedora-elections.conf \
# Install configuration file
mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/fedora-elections
-install -m 644 files/fedora-elections.cfg.sample \
+install -m 644 files/fedora-elections.cfg \
$RPM_BUILD_ROOT/%{_sysconfdir}/fedora-elections/fedora-elections.cfg
# Install WSGI file
commit 1e19ee637b8c0924f5583417eaf12280fd17bdc2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Apr 8 17:47:21 2014 +0200
Fix the createdb.py script regarding jinja2
diff --git a/createdb.py b/createdb.py
index af15e2d..89a4a1e 100644
--- a/createdb.py
+++ b/createdb.py
@@ -1,5 +1,5 @@
import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7']
+__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
import pkg_resources
from fedora_elections import APP
commit a4bcd88723addc61ba6d554f6eb4b1342b51a2a0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon Apr 7 22:42:33 2014 +0200
The spec file has moved to the files folder
diff --git a/MANIFEST.in b/MANIFEST.in
index 5910b03..c6b69a5 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,4 @@
include AUTHORS COPYING README requirements.txt
-include fedora_electiosn.spec
include createdb.py
recursive-include fedora_elections *
recursive-include files *
commit 3a69e090d3949495e8c4f5287b646906869c6f94
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon Apr 7 10:25:12 2014 +0200
Add example configuration file explaining as well all the configuration keys
diff --git a/files/fedora-elections.cfg b/files/fedora-elections.cfg
new file mode 100644
index 0000000..0ce7c61
--- /dev/null
+++ b/files/fedora-elections.cfg
@@ -0,0 +1,28 @@
+## Example configuration file for fedora-elections
+
+
+## The secret key used by the Flask application, change it upon install
+SECRET_KEY = 'change me'
+
+
+## URL to the database server:
+#DB_URL=mysql://user:pass@host/db_name
+#DB_URL=postgres://user:pass@host/db_name
+DB_URL = 'sqlite:////var/tmp/elections_dev.sqlite'
+
+
+## One ore more FAS group that will have admin rights over the whole
+## application, including all elections past, present and future
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+
+
+## Fedora-elections can integrate with FAS to retrieve information about the
+## candidates, the following configuration keys are required for this
+## integration.
+## URL of the FAS server to use
+FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
+FAS_USERNAME = ''
+FAS_PASSWORD = ''
+## This should be ``True`` but should be changed to ``False`` if the FAS server
+## used does not have a valid SSL certificate.
+FAS_CHECK_CERT = True
commit 7658d61f780e0f0196facd3274cab88efb110958
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat Apr 5 10:31:15 2014 +0200
Simplify the .gitignore file
Most of these files are and will not in this directory
diff --git a/.gitignore b/.gitignore
index 2ff6ed6..da17908 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,13 +1,6 @@
-database.sqlite
*.pyc
*.pyo
*.swp
*.egg-info
build/
dist/
-
-createdb
-development.cfg
-fedora-elections.cfg
-fedora-elections.wsgi
-httpd-fedora-elections.conf
commit c59ba3cfdddf2a24e1da36750162827a5e1be16f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat Apr 5 10:29:22 2014 +0200
Ignore the build and dist folders from the sources
diff --git a/.gitignore b/.gitignore
index f93993b..2ff6ed6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,9 @@ database.sqlite
*.pyo
*.swp
*.egg-info
+build/
+dist/
+
createdb
development.cfg
fedora-elections.cfg
commit f0c91e9eb0e3024040773bb1e39ead7ae29de51f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Apr 4 10:28:31 2014 +0200
Update the spec file to the changes made in the setup.py and directory structure
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
index 4977cb3..08c019c 100644
--- a/files/fedora-elections.spec
+++ b/files/fedora-elections.spec
@@ -1,7 +1,4 @@
-%{!?pyver: %define pyver %(%{__python} -c "import sys ; print sys.version[:3]")}
-
%define modname fedora_elections
-%define eggname fedora_elections
Name: fedora-elections
Version: 0.2
@@ -12,115 +9,74 @@ Group: Development/Languages
License: GPLv2+
URL: https://github.com/fedora-infra/elections
Source0: %{name}/%{name}-%{version}.tar.gz
-# Source0: https://fedorahosted.org/releases/f/a/%{name}/%{name}-%{version}.tar.gz
+#Source0: https://fedorahosted.org/releases/f/a/%{name}/%{name}-%{version}.tar.gz
BuildArch: noarch
-BuildRequires: libxslt
-BuildRequires: python
-BuildRequires: python-babel
-BuildRequires: python-backports-ssl_match_hostname
-BuildRequires: python-bunch
-BuildRequires: python-chardet
BuildRequires: python-fedora
BuildRequires: python-fedora-flask
-BuildRequires: python-flask
-BuildRequires: python-flask-sqlalchemy
BuildRequires: python-flask-wtf
-BuildRequires: python-jinja2
BuildRequires: python-kitchen
-BuildRequires: python-lxml
BuildRequires: python-openid
-BuildRequires: python-ordereddict
-BuildRequires: python-requests
+BuildRequires: python-openid-teams
+BuildRequires: python-openid-cla
BuildRequires: python-setuptools
-BuildRequires: python-simplejson
-BuildRequires: python-six
-BuildRequires: python-sqlalchemy0.7
-BuildRequires: python-urllib3
BuildRequires: python-wtforms
-Requires: libxslt
-Requires: python
-Requires: python-babel
-Requires: python-backports-ssl_match_hostname
-Requires: python-bunch
-Requires: python-chardet
Requires: python-fedora
Requires: python-fedora-flask
-Requires: python-flask
-Requires: python-flask-sqlalchemy
Requires: python-flask-wtf
-Requires: python-jinja2
Requires: python-kitchen
-Requires: python-lxml
Requires: python-openid
-Requires: python-ordereddict
-Requires: python-requests
-Requires: python-setuptools
-Requires: python-simplejson
-Requires: python-six
-Requires: python-sqlalchemy0.7
-Requires: python-urllib3
+Requires: python-openid-teams
+Requires: python-openid-cla
Requires: python-wtforms
+# EPEL6
+%if ( 0%{?rhel} && 0%{?rhel} == 6 )
+BuildRequires: python-sqlalchemy0.7
+Requires: python-sqlalchemy0.7
+%else
+BuildRequires: python-sqlalchemy > 0.5
+Requires: python-sqlalchemy > 0.5
+%endif
+
%description
fedora-elections is the Fedora Elections application.
%prep
%setup -q
-cd fedora_elections
-../configure.py
%build
-# Remove CFLAGS=... for noarch packages (unneeded)
-CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
+%{__python} setup.py build
%install
rm -rf $RPM_BUILD_ROOT
%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT
-%{__mkdir_p} %{buildroot}%{_sysconfdir}/%{name}
-%{__install} -m 444 fedora_elections/%{name}.cfg \
- %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
-
-%{__mkdir_p} %{buildroot}%{_sysconfdir}/httpd/conf.d
-%{__install} -m 444 fedora_elections/httpd-fedora-elections.conf \
- %{buildroot}%{_sysconfdir}/httpd/conf.d/httpd-fedora-elections.conf
-
-%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}
-%{__install} -m 544 fedora_elections/createdb %{buildroot}%{_datadir}/%{modname}/createdb
-%{__install} -m 544 fedora_elections/createdb.py \
- %{buildroot}%{_datadir}/%{modname}/createdb.py
-%{__install} -m 444 fedora_elections/%{name}.wsgi \
- %{buildroot}%{_datadir}/%{modname}/%{name}.wsgi
-
-%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/static
-%{__install} -m 444 fedora_elections/static/* %{buildroot}%{_datadir}/%{modname}/static
-
-%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/images
-%{__install} -m 444 fedora_elections/images/* %{buildroot}%{_datadir}/%{modname}/images
-
-%{__mkdir_p} %{buildroot}%{_sharedstatedir}/%{modname}
-
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/admin
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/auth
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/election
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/list
-%{__install} -m 444 fedora_elections/templates/*.html \
- %{buildroot}%{python_sitelib}/%{modname}/templates
-%{__install} -m 444 fedora_elections/templates/admin/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/admin
-%{__install} -m 444 fedora_elections/templates/auth/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/auth
-%{__install} -m 444 fedora_elections/templates/election/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/election
-%{__install} -m 444 fedora_elections/templates/list/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/list
+
+# Install apache configuration file
+mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/httpd/conf.d/
+install -m 644 files/fedora-elections.conf \
+ $RPM_BUILD_ROOT/%{_sysconfdir}/httpd/conf.d/fedora-elections.conf
+
+# Install configuration file
+mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/fedora-elections
+install -m 644 files/fedora-elections.cfg.sample \
+ $RPM_BUILD_ROOT/%{_sysconfdir}/fedora-elections/fedora-elections.cfg
+
+# Install WSGI file
+mkdir -p $RPM_BUILD_ROOT/%{_datadir}/fedora-elections
+install -m 644 files/fedora-elections.wsgi \
+ $RPM_BUILD_ROOT/%{_datadir}/fedora-elections/fedora-elections.wsgi
+
+# Install the createdb script
+install -m 644 createdb.py \
+ $RPM_BUILD_ROOT/%{_datadir}/fedora-elections/fedora-elections_createdb.py
+
%clean
@@ -131,51 +87,19 @@ rm -rf $RPM_BUILD_ROOT
%defattr(-,root,root,-)
%doc
%dir %{_sysconfdir}/%{name}
-%attr(744,apache,apache) %{_sharedstatedir}/%{modname}
-%{_datadir}/%{modname}/
%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
-%config(noreplace) %{_sysconfdir}/httpd/conf.d/httpd-%{name}.conf
+%config(noreplace) %{_sysconfdir}/httpd/conf.d/%{name}.conf
%{python_sitelib}/%{modname}/
-%{python_sitelib}/%{eggname}-%{version}-py%{pyver}.egg-info/
-
-
-%post
-cd %{_sharedstatedir}/%{modname}
-if [[ ! -f database.sqlite ]]
-then
- #
- # The database does not exist create one.
- #
- echo " Creating a database."
- %{_datadir}/%{modname}/createdb
- echo " Changing permissions."
- chmod 644 database.sqlite
- echo " Changing owner."
- chown apache:apache database.sqlite
-fi
-
-
-%postun
-cd %{_sharedstatedir}/%{modname}
-if [[ -f database.sqlite ]]
-then
- #
- # Remove the database.
- #
- echo " Removing the database."
- /bin/rm database.sqlite
-fi
-
-cd ${_sharedstatedir}
-if [[ -d %{_sharedstatedir}/%{modname} ]]
-then
- /bin/rmdir %{_sharedstatedir}/%{modname}
-fi
+%{python_sitelib}/%{modname}*.egg-info/
%changelog
-* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.2
+* Thu Apr 03 2014 Pierre-Yves Chibon <pingou(a)pingoured.fr> - 0.2-2
+- Clean up the spec with the changes made on the setup.py
+- Remove instanciated a sqlite database
+
+* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.2-1
- Simple voting
-* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1
+* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1-1
- Creation
commit f241ae8a05559b9a366141b8445fe28d84a7bbba
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Apr 4 10:18:47 2014 +0200
Include a MANIFEST.in files listing which files should be present in the releases
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..5910b03
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,6 @@
+include AUTHORS COPYING README requirements.txt
+include fedora_electiosn.spec
+include createdb.py
+recursive-include fedora_elections *
+recursive-include files *
+recursive-include tests *
commit 6d44730c62f9eb09a4b57ad36c20af9f8f482031
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Apr 4 10:18:41 2014 +0200
Update the setup.py script
- Update the list of dependencies
- pep8 fixes
diff --git a/setup.py b/setup.py
index b05b39b..1b3a80a 100644
--- a/setup.py
+++ b/setup.py
@@ -1,18 +1,27 @@
+#!/usr/bin/env python
+
+"""
+Setup script
+"""
+
## These two lines are needed to run on EL6
__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
import pkg_resources
from fedora_elections import __version__
-from setuptools import setup, find_packages
+from setuptools import setup
setup(
- name = 'fedora-elections',
- version = __version__,
- author = 'Frank Chiulli',
- author_email = 'fchiulli(a)fedoraproject.org',
- packages = find_packages(),
- zip_safe = False,
- include_package_data = True,
- install_requires = ['Flask', 'SQLAlchemy>=0.7', 'python-fedora'],
+ name='fedora-elections',
+ version=__version__,
+ author='Frank Chiulli',
+ author_email='fchiulli(a)fedoraproject.org',
+ packages=['fedora_elections'],
+ include_package_data=True,
+ install_requires=[
+ 'Flask', 'SQLAlchemy>=0.7', 'python-fedora', 'python-kitchen',
+ 'python-openid', 'python-openid-teams', 'python-openid-cla',
+ 'Flask-wtf', 'wtforms',
+ ],
)
commit f7c521c043be4d03c8a0444debaaf53a4e056e41
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Apr 4 10:40:38 2014 +0200
Remove configuration key that is not used anywhere
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
index 9f3a785..fcdbea0 100644
--- a/fedora_elections/default_config.py
+++ b/fedora_elections/default_config.py
@@ -12,6 +12,3 @@ FAS_USERNAME = ''
FAS_PASSWORD = ''
FAS_CHECK_CERT = False
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
-# List of URLs for additional site-wide CSS
-EXTRA_CSS = []
commit 1e77bab367e374ddd430456f28c0f963aecd3055
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:14:29 2014 +0200
Remove the `shell` script now that there is no longer development.cfg file
Since we have no longer `development.cfg` file, the shell script becomes
a simple call to either ipython or python, so there is no longer interest
in keeping it around.
diff --git a/shell b/shell
deleted file mode 100755
index fd475b5..0000000
--- a/shell
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-which ipython > /dev/null 2>&1
-if [ "$?" == "0" ]; then
- FEDORA_ELECTIONS_CONFIG=../development.cfg ipython
-else
- FEDORA_ELECTIONS_CONFIG=../development.cfg python
-fi
commit 9ef2c9ec225542d8ccb8161fb3901ee359b692c3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:13:09 2014 +0200
Rename the example wsgi file for fedora_elections
diff --git a/files/fedora-elections.wsgi b/files/fedora-elections.wsgi
new file mode 100644
index 0000000..74ca155
--- /dev/null
+++ b/files/fedora-elections.wsgi
@@ -0,0 +1,19 @@
+#-*- coding: UTF-8 -*-
+
+# These two lines are needed to run on EL6
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import sys
+sys.stdout = sys.stderr
+
+## Set the environment variable pointing to the configuration file
+#import os
+#os.environ['FEDORA_ELECTIONS_CONFIG'] = '/etc/fedora-elections/fedora-elections.cfg'
+
+## The following is only needed if you did not install
+## as a python module (for example if you run it from a git clone).
+#sys.path.insert(0, '/path/to/fedora_elections/')
+
+from fedora_elections import app as application
diff --git a/files/fedora-elections.wsgi.in b/files/fedora-elections.wsgi.in
deleted file mode 100644
index 74ca155..0000000
--- a/files/fedora-elections.wsgi.in
+++ /dev/null
@@ -1,19 +0,0 @@
-#-*- coding: UTF-8 -*-
-
-# These two lines are needed to run on EL6
-import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
-import pkg_resources
-
-import sys
-sys.stdout = sys.stderr
-
-## Set the environment variable pointing to the configuration file
-#import os
-#os.environ['FEDORA_ELECTIONS_CONFIG'] = '/etc/fedora-elections/fedora-elections.cfg'
-
-## The following is only needed if you did not install
-## as a python module (for example if you run it from a git clone).
-#sys.path.insert(0, '/path/to/fedora_elections/')
-
-from fedora_elections import app as application
commit 66f0b825dbfdd59de4346a3954c73351317fb2bf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:12:39 2014 +0200
Small adjustments to the example wsgi file
diff --git a/files/fedora-elections.wsgi.in b/files/fedora-elections.wsgi.in
index 430cc5c..74ca155 100644
--- a/files/fedora-elections.wsgi.in
+++ b/files/fedora-elections.wsgi.in
@@ -5,17 +5,15 @@ import __main__
__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
import pkg_resources
-import os
import sys
-
sys.stdout = sys.stderr
## Set the environment variable pointing to the configuration file
-os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
+#import os
+#os.environ['FEDORA_ELECTIONS_CONFIG'] = '/etc/fedora-elections/fedora-elections.cfg'
## The following is only needed if you did not install
## as a python module (for example if you run it from a git clone).
-import sys
-# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
+#sys.path.insert(0, '/path/to/fedora_elections/')
from fedora_elections import app as application
commit a08596b9d1d27020659122131295a2248255dcf1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:10:39 2014 +0200
Rename the example apache configuration file
diff --git a/files/fedora-elections.conf b/files/fedora-elections.conf
new file mode 100644
index 0000000..dc905fb
--- /dev/null
+++ b/files/fedora-elections.conf
@@ -0,0 +1,27 @@
+# Example apache configuration file for fedora-elections
+#
+# Adjust as needed, including replacing @DATADIR@ by what it should be
+
+#Alias /fedora-elections/images @DATADIR@/fedora_elections/images
+#Alias /fedora-elections/static @DATADIR@/fedora_elections/static
+#
+#WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
+#WSGISocketPrefix run/wsgi
+#WSGIRestrictStdout On
+#WSGIRestrictSignal Off
+#WSGIPythonOptimize 1
+#
+#WSGIScriptAlias /voting @DATADIR(a)/fedora_elections/fedora-elections.wsgi
+#
+#<Location />
+# WSGIProcessGroup fedora-elections
+# <IfModule mod_authz_core.c>
+# # Apache 2.4
+# Require all granted
+# </IfModule>
+# <IfModule !mod_authz_core.c>
+# # Apache 2.2
+# Order deny,allow
+# Allow from all
+# </IfModule>
+#</Location>
diff --git a/files/httpd-fedora-elections.conf.in b/files/httpd-fedora-elections.conf.in
deleted file mode 100644
index dc905fb..0000000
--- a/files/httpd-fedora-elections.conf.in
+++ /dev/null
@@ -1,27 +0,0 @@
-# Example apache configuration file for fedora-elections
-#
-# Adjust as needed, including replacing @DATADIR@ by what it should be
-
-#Alias /fedora-elections/images @DATADIR@/fedora_elections/images
-#Alias /fedora-elections/static @DATADIR@/fedora_elections/static
-#
-#WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
-#WSGISocketPrefix run/wsgi
-#WSGIRestrictStdout On
-#WSGIRestrictSignal Off
-#WSGIPythonOptimize 1
-#
-#WSGIScriptAlias /voting @DATADIR(a)/fedora_elections/fedora-elections.wsgi
-#
-#<Location />
-# WSGIProcessGroup fedora-elections
-# <IfModule mod_authz_core.c>
-# # Apache 2.4
-# Require all granted
-# </IfModule>
-# <IfModule !mod_authz_core.c>
-# # Apache 2.2
-# Order deny,allow
-# Allow from all
-# </IfModule>
-#</Location>
commit 89816f0c62682b37ac922c989d983c4a186dc1c9
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:09:51 2014 +0200
The apache configuration file should be commented out by default
We do not want to have the application running by itself after having
installed the files/rpm.
diff --git a/files/httpd-fedora-elections.conf.in b/files/httpd-fedora-elections.conf.in
index c79d2d9..dc905fb 100644
--- a/files/httpd-fedora-elections.conf.in
+++ b/files/httpd-fedora-elections.conf.in
@@ -1,23 +1,27 @@
-Alias /fedora-elections/images @DATADIR@/fedora_elections/images
-Alias /fedora-elections/static @DATADIR@/fedora_elections/static
+# Example apache configuration file for fedora-elections
+#
+# Adjust as needed, including replacing @DATADIR@ by what it should be
-WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
-WSGISocketPrefix run/wsgi
-WSGIRestrictStdout On
-WSGIRestrictSignal Off
-WSGIPythonOptimize 1
-
-WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora_elections/fedora-elections.wsgi
-
-<Location /fedora-elections>
- WSGIProcessGroup fedora-elections
- <IfModule mod_authz_core.c>
- # Apache 2.4
- Require all granted
- </IfModule>
- <IfModule !mod_authz_core.c>
- # Apache 2.2
- Order deny,allow
- Allow from all
- </IfModule>
-</Location>
+#Alias /fedora-elections/images @DATADIR@/fedora_elections/images
+#Alias /fedora-elections/static @DATADIR@/fedora_elections/static
+#
+#WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
+#WSGISocketPrefix run/wsgi
+#WSGIRestrictStdout On
+#WSGIRestrictSignal Off
+#WSGIPythonOptimize 1
+#
+#WSGIScriptAlias /voting @DATADIR(a)/fedora_elections/fedora-elections.wsgi
+#
+#<Location />
+# WSGIProcessGroup fedora-elections
+# <IfModule mod_authz_core.c>
+# # Apache 2.4
+# Require all granted
+# </IfModule>
+# <IfModule !mod_authz_core.c>
+# # Apache 2.2
+# Order deny,allow
+# Allow from all
+# </IfModule>
+#</Location>
commit 9c5db3417914a83933af52aa086cb19915f1f07a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:07:05 2014 +0200
Remove the no-longer needed configure script
diff --git a/configure.py b/configure.py
deleted file mode 100755
index 66d899f..0000000
--- a/configure.py
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/usr/bin/python -tt
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2013 Frank Chiulli <fchiulli(a)fedoraproject.org>
-#
-# This copyrighted material is made available to anyone wishing to use, modify,
-# copy, or redistribute it subject to the terms and conditions of the GNU
-# General Public License v.2, or (at your option) any later version. This
-# program is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY expressed or implied, including the implied warranties of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
-# Public License for more details. You should have received a copy of the GNU
-# General Public License along with this program; if not, write to the Free
-# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
-# code or documentation are not subject to the GNU General Public License and
-# may only be used or replicated with the express permission of Red Hat, Inc.
-#
-# Author(s): Frank Chiulli <fchiulli(a)fedoraproject.org>
-#
-
-import glob
-import os
-import re
-
-from optparse import OptionParser
-
-#
-# Process any command line options/switches.
-#
-parser = OptionParser()
-parser.add_option('--install-conf', dest='confdir', action='store',
- default='/etc',
- help='Where to install configuration files.')
-parser.add_option('--install-data', dest='datadir', action='store',
- default='/usr/share',
- help='Where to install data files.')
-parser.add_option('--shared-state', dest='sharedstdir', action='store',
- default='/var/lib',
- help='Where to install shared state files.')
-
-(options, args) = parser.parse_args()
-
-confdir = options.confdir
-datadir = options.datadir
-sharedstdir = options.sharedstdir
-
-print " confdir = %s" % confdir
-print " datadir = %s" % datadir
-print " sharedstdir = %s" % sharedstdir
-
-substs = {'@CONFDIR@':confdir,
- '@DATADIR@':datadir,
- '@SHAREDSTDIR@':sharedstdir
- }
-
-pattern = re.compile("\.in$")
-
-files = glob.glob('./*.in')
-
-#
-# Process all *.in files in the current directory.
-#
-if (files is not []):
- for orig_fn in files:
- if os.path.isfile(orig_fn):
- new_fn = re.sub(pattern, '', orig_fn)
- print " Creating %s from %s" % (new_fn, orig_fn)
- fp1 = open(orig_fn, "r")
- file_contents = fp1.read()
- fp1.close()
-
- fp2 = open(new_fn, "w")
- for key, value in substs.iteritems():
- file_contents = file_contents.replace(key, value)
- fp2.write(file_contents)
- fp2.close()
commit c937be33d30fca01e5b2f4c5e69c3d31cde702bf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:06:33 2014 +0200
Ignore .egg-info files in the git
diff --git a/.gitignore b/.gitignore
index ce498f2..f93993b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@ database.sqlite
*.pyc
*.pyo
*.swp
+*.egg-info
createdb
development.cfg
fedora-elections.cfg
commit 91cdfc5f9dcd85204c28d33cb2afce796c06ce3a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:05:59 2014 +0200
Move the spec file and the sample configuration file into the files folder
diff --git a/fedora-elections.spec b/fedora-elections.spec
deleted file mode 100644
index 4977cb3..0000000
--- a/fedora-elections.spec
+++ /dev/null
@@ -1,181 +0,0 @@
-%{!?pyver: %define pyver %(%{__python} -c "import sys ; print sys.version[:3]")}
-
-%define modname fedora_elections
-%define eggname fedora_elections
-
-Name: fedora-elections
-Version: 0.2
-Release: 1%{?dist}
-Summary: Fedora elections application
-
-Group: Development/Languages
-License: GPLv2+
-URL: https://github.com/fedora-infra/elections
-Source0: %{name}/%{name}-%{version}.tar.gz
-# Source0: https://fedorahosted.org/releases/f/a/%{name}/%{name}-%{version}.tar.gz
-
-BuildArch: noarch
-
-BuildRequires: libxslt
-BuildRequires: python
-BuildRequires: python-babel
-BuildRequires: python-backports-ssl_match_hostname
-BuildRequires: python-bunch
-BuildRequires: python-chardet
-BuildRequires: python-fedora
-BuildRequires: python-fedora-flask
-BuildRequires: python-flask
-BuildRequires: python-flask-sqlalchemy
-BuildRequires: python-flask-wtf
-BuildRequires: python-jinja2
-BuildRequires: python-kitchen
-BuildRequires: python-lxml
-BuildRequires: python-openid
-BuildRequires: python-ordereddict
-BuildRequires: python-requests
-BuildRequires: python-setuptools
-BuildRequires: python-simplejson
-BuildRequires: python-six
-BuildRequires: python-sqlalchemy0.7
-BuildRequires: python-urllib3
-BuildRequires: python-wtforms
-
-Requires: libxslt
-Requires: python
-Requires: python-babel
-Requires: python-backports-ssl_match_hostname
-Requires: python-bunch
-Requires: python-chardet
-Requires: python-fedora
-Requires: python-fedora-flask
-Requires: python-flask
-Requires: python-flask-sqlalchemy
-Requires: python-flask-wtf
-Requires: python-jinja2
-Requires: python-kitchen
-Requires: python-lxml
-Requires: python-openid
-Requires: python-ordereddict
-Requires: python-requests
-Requires: python-setuptools
-Requires: python-simplejson
-Requires: python-six
-Requires: python-sqlalchemy0.7
-Requires: python-urllib3
-Requires: python-wtforms
-
-
-%description
-fedora-elections is the Fedora Elections application.
-
-%prep
-%setup -q
-cd fedora_elections
-../configure.py
-
-
-%build
-# Remove CFLAGS=... for noarch packages (unneeded)
-CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
-
-
-%install
-rm -rf $RPM_BUILD_ROOT
-%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT
-
-%{__mkdir_p} %{buildroot}%{_sysconfdir}/%{name}
-%{__install} -m 444 fedora_elections/%{name}.cfg \
- %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
-
-%{__mkdir_p} %{buildroot}%{_sysconfdir}/httpd/conf.d
-%{__install} -m 444 fedora_elections/httpd-fedora-elections.conf \
- %{buildroot}%{_sysconfdir}/httpd/conf.d/httpd-fedora-elections.conf
-
-%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}
-%{__install} -m 544 fedora_elections/createdb %{buildroot}%{_datadir}/%{modname}/createdb
-%{__install} -m 544 fedora_elections/createdb.py \
- %{buildroot}%{_datadir}/%{modname}/createdb.py
-%{__install} -m 444 fedora_elections/%{name}.wsgi \
- %{buildroot}%{_datadir}/%{modname}/%{name}.wsgi
-
-%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/static
-%{__install} -m 444 fedora_elections/static/* %{buildroot}%{_datadir}/%{modname}/static
-
-%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/images
-%{__install} -m 444 fedora_elections/images/* %{buildroot}%{_datadir}/%{modname}/images
-
-%{__mkdir_p} %{buildroot}%{_sharedstatedir}/%{modname}
-
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/admin
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/auth
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/election
-%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/list
-%{__install} -m 444 fedora_elections/templates/*.html \
- %{buildroot}%{python_sitelib}/%{modname}/templates
-%{__install} -m 444 fedora_elections/templates/admin/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/admin
-%{__install} -m 444 fedora_elections/templates/auth/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/auth
-%{__install} -m 444 fedora_elections/templates/election/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/election
-%{__install} -m 444 fedora_elections/templates/list/* \
- %{buildroot}%{python_sitelib}/%{modname}/templates/list
-
-
-%clean
-rm -rf $RPM_BUILD_ROOT
-
-
-%files
-%defattr(-,root,root,-)
-%doc
-%dir %{_sysconfdir}/%{name}
-%attr(744,apache,apache) %{_sharedstatedir}/%{modname}
-%{_datadir}/%{modname}/
-%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
-%config(noreplace) %{_sysconfdir}/httpd/conf.d/httpd-%{name}.conf
-%{python_sitelib}/%{modname}/
-%{python_sitelib}/%{eggname}-%{version}-py%{pyver}.egg-info/
-
-
-%post
-cd %{_sharedstatedir}/%{modname}
-if [[ ! -f database.sqlite ]]
-then
- #
- # The database does not exist create one.
- #
- echo " Creating a database."
- %{_datadir}/%{modname}/createdb
- echo " Changing permissions."
- chmod 644 database.sqlite
- echo " Changing owner."
- chown apache:apache database.sqlite
-fi
-
-
-%postun
-cd %{_sharedstatedir}/%{modname}
-if [[ -f database.sqlite ]]
-then
- #
- # Remove the database.
- #
- echo " Removing the database."
- /bin/rm database.sqlite
-fi
-
-cd ${_sharedstatedir}
-if [[ -d %{_sharedstatedir}/%{modname} ]]
-then
- /bin/rmdir %{_sharedstatedir}/%{modname}
-fi
-
-
-%changelog
-* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.2
-- Simple voting
-
-* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1
-- Creation
diff --git a/fedora_elections/fedora-elections.wsgi.in b/fedora_elections/fedora-elections.wsgi.in
deleted file mode 100644
index 430cc5c..0000000
--- a/fedora_elections/fedora-elections.wsgi.in
+++ /dev/null
@@ -1,21 +0,0 @@
-#-*- coding: UTF-8 -*-
-
-# These two lines are needed to run on EL6
-import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
-import pkg_resources
-
-import os
-import sys
-
-sys.stdout = sys.stderr
-
-## Set the environment variable pointing to the configuration file
-os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
-
-## The following is only needed if you did not install
-## as a python module (for example if you run it from a git clone).
-import sys
-# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
-
-from fedora_elections import app as application
diff --git a/fedora_elections/httpd-fedora-elections.conf.in b/fedora_elections/httpd-fedora-elections.conf.in
deleted file mode 100644
index c79d2d9..0000000
--- a/fedora_elections/httpd-fedora-elections.conf.in
+++ /dev/null
@@ -1,23 +0,0 @@
-Alias /fedora-elections/images @DATADIR@/fedora_elections/images
-Alias /fedora-elections/static @DATADIR@/fedora_elections/static
-
-WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
-WSGISocketPrefix run/wsgi
-WSGIRestrictStdout On
-WSGIRestrictSignal Off
-WSGIPythonOptimize 1
-
-WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora_elections/fedora-elections.wsgi
-
-<Location /fedora-elections>
- WSGIProcessGroup fedora-elections
- <IfModule mod_authz_core.c>
- # Apache 2.4
- Require all granted
- </IfModule>
- <IfModule !mod_authz_core.c>
- # Apache 2.2
- Order deny,allow
- Allow from all
- </IfModule>
-</Location>
diff --git a/files/fedora-elections.spec b/files/fedora-elections.spec
new file mode 100644
index 0000000..4977cb3
--- /dev/null
+++ b/files/fedora-elections.spec
@@ -0,0 +1,181 @@
+%{!?pyver: %define pyver %(%{__python} -c "import sys ; print sys.version[:3]")}
+
+%define modname fedora_elections
+%define eggname fedora_elections
+
+Name: fedora-elections
+Version: 0.2
+Release: 1%{?dist}
+Summary: Fedora elections application
+
+Group: Development/Languages
+License: GPLv2+
+URL: https://github.com/fedora-infra/elections
+Source0: %{name}/%{name}-%{version}.tar.gz
+# Source0: https://fedorahosted.org/releases/f/a/%{name}/%{name}-%{version}.tar.gz
+
+BuildArch: noarch
+
+BuildRequires: libxslt
+BuildRequires: python
+BuildRequires: python-babel
+BuildRequires: python-backports-ssl_match_hostname
+BuildRequires: python-bunch
+BuildRequires: python-chardet
+BuildRequires: python-fedora
+BuildRequires: python-fedora-flask
+BuildRequires: python-flask
+BuildRequires: python-flask-sqlalchemy
+BuildRequires: python-flask-wtf
+BuildRequires: python-jinja2
+BuildRequires: python-kitchen
+BuildRequires: python-lxml
+BuildRequires: python-openid
+BuildRequires: python-ordereddict
+BuildRequires: python-requests
+BuildRequires: python-setuptools
+BuildRequires: python-simplejson
+BuildRequires: python-six
+BuildRequires: python-sqlalchemy0.7
+BuildRequires: python-urllib3
+BuildRequires: python-wtforms
+
+Requires: libxslt
+Requires: python
+Requires: python-babel
+Requires: python-backports-ssl_match_hostname
+Requires: python-bunch
+Requires: python-chardet
+Requires: python-fedora
+Requires: python-fedora-flask
+Requires: python-flask
+Requires: python-flask-sqlalchemy
+Requires: python-flask-wtf
+Requires: python-jinja2
+Requires: python-kitchen
+Requires: python-lxml
+Requires: python-openid
+Requires: python-ordereddict
+Requires: python-requests
+Requires: python-setuptools
+Requires: python-simplejson
+Requires: python-six
+Requires: python-sqlalchemy0.7
+Requires: python-urllib3
+Requires: python-wtforms
+
+
+%description
+fedora-elections is the Fedora Elections application.
+
+%prep
+%setup -q
+cd fedora_elections
+../configure.py
+
+
+%build
+# Remove CFLAGS=... for noarch packages (unneeded)
+CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
+
+
+%install
+rm -rf $RPM_BUILD_ROOT
+%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT
+
+%{__mkdir_p} %{buildroot}%{_sysconfdir}/%{name}
+%{__install} -m 444 fedora_elections/%{name}.cfg \
+ %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
+
+%{__mkdir_p} %{buildroot}%{_sysconfdir}/httpd/conf.d
+%{__install} -m 444 fedora_elections/httpd-fedora-elections.conf \
+ %{buildroot}%{_sysconfdir}/httpd/conf.d/httpd-fedora-elections.conf
+
+%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}
+%{__install} -m 544 fedora_elections/createdb %{buildroot}%{_datadir}/%{modname}/createdb
+%{__install} -m 544 fedora_elections/createdb.py \
+ %{buildroot}%{_datadir}/%{modname}/createdb.py
+%{__install} -m 444 fedora_elections/%{name}.wsgi \
+ %{buildroot}%{_datadir}/%{modname}/%{name}.wsgi
+
+%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/static
+%{__install} -m 444 fedora_elections/static/* %{buildroot}%{_datadir}/%{modname}/static
+
+%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/images
+%{__install} -m 444 fedora_elections/images/* %{buildroot}%{_datadir}/%{modname}/images
+
+%{__mkdir_p} %{buildroot}%{_sharedstatedir}/%{modname}
+
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/admin
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/auth
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/election
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/list
+%{__install} -m 444 fedora_elections/templates/*.html \
+ %{buildroot}%{python_sitelib}/%{modname}/templates
+%{__install} -m 444 fedora_elections/templates/admin/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/admin
+%{__install} -m 444 fedora_elections/templates/auth/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/auth
+%{__install} -m 444 fedora_elections/templates/election/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/election
+%{__install} -m 444 fedora_elections/templates/list/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/list
+
+
+%clean
+rm -rf $RPM_BUILD_ROOT
+
+
+%files
+%defattr(-,root,root,-)
+%doc
+%dir %{_sysconfdir}/%{name}
+%attr(744,apache,apache) %{_sharedstatedir}/%{modname}
+%{_datadir}/%{modname}/
+%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
+%config(noreplace) %{_sysconfdir}/httpd/conf.d/httpd-%{name}.conf
+%{python_sitelib}/%{modname}/
+%{python_sitelib}/%{eggname}-%{version}-py%{pyver}.egg-info/
+
+
+%post
+cd %{_sharedstatedir}/%{modname}
+if [[ ! -f database.sqlite ]]
+then
+ #
+ # The database does not exist create one.
+ #
+ echo " Creating a database."
+ %{_datadir}/%{modname}/createdb
+ echo " Changing permissions."
+ chmod 644 database.sqlite
+ echo " Changing owner."
+ chown apache:apache database.sqlite
+fi
+
+
+%postun
+cd %{_sharedstatedir}/%{modname}
+if [[ -f database.sqlite ]]
+then
+ #
+ # Remove the database.
+ #
+ echo " Removing the database."
+ /bin/rm database.sqlite
+fi
+
+cd ${_sharedstatedir}
+if [[ -d %{_sharedstatedir}/%{modname} ]]
+then
+ /bin/rmdir %{_sharedstatedir}/%{modname}
+fi
+
+
+%changelog
+* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.2
+- Simple voting
+
+* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1
+- Creation
diff --git a/files/fedora-elections.wsgi.in b/files/fedora-elections.wsgi.in
new file mode 100644
index 0000000..430cc5c
--- /dev/null
+++ b/files/fedora-elections.wsgi.in
@@ -0,0 +1,21 @@
+#-*- coding: UTF-8 -*-
+
+# These two lines are needed to run on EL6
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import os
+import sys
+
+sys.stdout = sys.stderr
+
+## Set the environment variable pointing to the configuration file
+os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
+
+## The following is only needed if you did not install
+## as a python module (for example if you run it from a git clone).
+import sys
+# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
+
+from fedora_elections import app as application
diff --git a/files/httpd-fedora-elections.conf.in b/files/httpd-fedora-elections.conf.in
new file mode 100644
index 0000000..c79d2d9
--- /dev/null
+++ b/files/httpd-fedora-elections.conf.in
@@ -0,0 +1,23 @@
+Alias /fedora-elections/images @DATADIR@/fedora_elections/images
+Alias /fedora-elections/static @DATADIR@/fedora_elections/static
+
+WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
+WSGISocketPrefix run/wsgi
+WSGIRestrictStdout On
+WSGIRestrictSignal Off
+WSGIPythonOptimize 1
+
+WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora_elections/fedora-elections.wsgi
+
+<Location /fedora-elections>
+ WSGIProcessGroup fedora-elections
+ <IfModule mod_authz_core.c>
+ # Apache 2.4
+ Require all granted
+ </IfModule>
+ <IfModule !mod_authz_core.c>
+ # Apache 2.2
+ Order deny,allow
+ Allow from all
+ </IfModule>
+</Location>
commit b71e6e7f730a3ee83fc51f7d32947ceb28861299
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 17:07:36 2014 +0100
No need for the runserver script, elections can now run with the default configuration
diff --git a/runserver b/runserver
deleted file mode 100755
index 39898a2..0000000
--- a/runserver
+++ /dev/null
@@ -1 +0,0 @@
-FEDORA_ELECTIONS_CONFIG=../development.cfg python runserver.py
commit 0dff882a6d655e2ad757175a63d267a4086fbbdd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 17:04:48 2014 +0100
Add a generic is_election_admin checking if the user is in the admin group set for an election
Use this generic function when checking if the user is allowed to access
embargoed results.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index deecb05..3350cb4 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -82,6 +82,24 @@ def is_admin(user):
return len(set(user.groups).intersection(admins)) > 0
+def is_election_admin(user, election_id):
+ ''' Check if the provided user is in one of the admin group of the
+ specified election.
+ '''
+ if not user:
+ return False
+ if not user.cla_done or len(user.groups) < 1:
+ return False
+
+ admingroups = [
+ group.group_name
+ for group in models.ElectionAdminGroup.by_election_id(
+ SESSION, election_id=election_id)
+ ]
+
+ return len(set(user.groups).intersection(set(admingroups))) > 0
+
+
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
@@ -671,25 +689,18 @@ def election_results(election_alias):
"embargoed pending formal announcement.")
return redirect.safe_redirect_back()
else:
- if is_admin(flask.g.fas_user):
+ if is_admin(flask.g.fas_user) \
+ or is_election_admin(flask.g.fas_user, election.id):
flask.flash("The results for this election are currently "
"embargoed pending formal announcement.",
"error")
pass
else:
- match = 0
- admingroups = models.ElectionAdminGroup.by_election_id(
- SESSION, election_id=election.id)
- for admingroup in admingroups:
- if admingroup.group_name in flask.g.fas_user.groups:
- match = 1
-
- if match == 0:
- flask.flash(
- "We are sorry. The results for this election"
- "cannot be viewed because they are currently "
- "embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
+ flask.flash(
+ "We are sorry. The results for this election"
+ "cannot be viewed because they are currently "
+ "embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
usernamemap = {}
if (election.candidates_are_fasusers):
commit 9ee093bbc43a52cfef2ebcea16f7c4c3e535282f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 17:01:53 2014 +0100
Rename the is_elections_admin function to is_admin
This is more appropriate since that function relies on the admin group
set in the configuration file.
is_elections_admin should rely on the information set on the database
in relation to a specific election.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 31858a2..deecb05 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -65,7 +65,7 @@ from fedora_elections import forms
from fedora_elections import redirect
-def is_elections_admin(user):
+def is_admin(user):
''' Is the user an elections admin.
'''
if not user:
@@ -98,7 +98,7 @@ def election_admin_required(f):
if not hasattr(flask.g, 'fas_user') or flask.g.fas_user is None:
return flask.redirect(flask.url_for(
'auth_login', next=flask.request.url))
- if not is_elections_admin(flask.g.fas_user):
+ if not is_admin(flask.g.fas_user):
flask.abort(403)
return f(*args, **kwargs)
return decorated_function
@@ -141,7 +141,7 @@ def inject_variables():
user = None
if hasattr(flask.g, 'fas_user'):
user = flask.g.fas_user
- return dict(is_admin=is_elections_admin(user),
+ return dict(is_admin=is_admin(user),
version=__version__)
@@ -671,8 +671,7 @@ def election_results(election_alias):
"embargoed pending formal announcement.")
return redirect.safe_redirect_back()
else:
- if APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
- flask.g.fas_user.groups:
+ if is_admin(flask.g.fas_user):
flask.flash("The results for this election are currently "
"embargoed pending formal announcement.",
"error")
commit f59de35c56be11990b41784d16f6d80cb8a0bdcd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:34:23 2014 +0100
Clean configuration file
- Change from fakefas to stg
- Remove username
- Remove password
- Remove unused configuration key
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
index bfa6d01..9f3a785 100644
--- a/fedora_elections/default_config.py
+++ b/fedora_elections/default_config.py
@@ -7,11 +7,10 @@ DB_URL = 'sqlite:////var/tmp/elections_dev.sqlite'
# You will want to change this for your install
SECRET_KEY = 'change me'
-FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
-FAS_USERNAME = 'elections'
-FAS_PASSWORD = 'el3ctMEn0w'
+FAS_BASE_URL = 'https://admin.stg.fedoraproject.org/accounts/'
+FAS_USERNAME = ''
+FAS_PASSWORD = ''
FAS_CHECK_CERT = False
-FAS_HTTPS_REQUIRED = False
FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
# List of URLs for additional site-wide CSS
commit 878acba3aea3593e3b2150719a4522e00c9d3939
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:30:34 2014 +0100
Fix the FAS2 client insecure settings
The boolean in the configuration file specify whether to check or not
the ssl cert, but the insecure works the other way around.
If you want to check the ssl cert, you do not want to be insecure and
vice-versa.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 2a16921..31858a2 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -54,7 +54,7 @@ FAS2 = AccountSystem(
APP.config['FAS_BASE_URL'],
username=APP.config['FAS_USERNAME'],
password=APP.config['FAS_PASSWORD'],
- insecure=APP.config['FAS_CHECK_CERT']
+ insecure= not APP.config['FAS_CHECK_CERT']
)
commit 071feb321bd44677dfe113fe22bf1ee2766bd7e4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:13:33 2014 +0100
Fix the status property of an election
In the election creation form we indicate that the end_date is included
in the voting period (vote ends on end_date at 23:59:59).
This commit fixes this by checking dates instead of date/time and making
sure the end_date is included in the voting period.
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 88773ed..e482dec 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -101,10 +101,10 @@ class Election(BASE):
@property
def status(self):
now = datetime.utcnow()
- if now < self.start_date:
+ if now.date() < self.start_date.date():
return 'Pending'
else:
- if now < self.end_date:
+ if now.date() <= self.end_date.date():
return 'In progress'
else:
if self.embargoed:
commit 5ca43cc0c569acd4233494b899d994ac8771c32e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:08:57 2014 +0100
Provide in the election result page some statistics about the election
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 9d7d9e7..2a16921 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -702,7 +702,11 @@ def election_results(election_alias):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
+ stats = models.Vote.get_election_stats(SESSION, election.id)
+
return flask.render_template(
'election/results.html',
election=election,
- usernamemap=usernamemap)
+ usernamemap=usernamemap,
+ stats=stats,
+ )
diff --git a/fedora_elections/templates/election/results.html b/fedora_elections/templates/election/results.html
index f61d214..9328b1f 100644
--- a/fedora_elections/templates/election/results.html
+++ b/fedora_elections/templates/election/results.html
@@ -6,6 +6,10 @@
<p><a href="{{ election.url }}">[More Information]</a></p>
{% endif %}
<table border="1" cellpadding="1">
+ <tr>
+ <th>Candidate</th>
+ <th title="Number of votes received">Votes</th>
+ </tr>
{% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
<tr>
<td>
@@ -22,4 +26,36 @@
</tr>
{% endfor %}
</table>
+
+ <section class="stats">
+ <h3>Some statistics about this election</h3>
+ <table>
+ <tr>
+ <th>Number of voters</th>
+ <td>{{ stats['n_voters'] }}</td>
+ </tr>
+ <tr>
+ <th title="Number of votes registered, there may be more than one vote per voters">Number of votes</th>
+ <td>{{ stats['n_votes'] }}</td>
+ </tr>
+ <tr>
+ <th title="Maximum number of votes possible">Maximum of votes</th>
+ <td>{{ stats['n_voters'] * stats['n_candidates'] }}</td>
+ </tr>
+ {% if stats['candidate_voters'] %}
+ <tr>
+ <th></th>
+ <th>Number of voters per candidate</th>
+ <th>Average votes per candidate</th>
+ </tr>
+ {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
+ <tr>
+ <th>{{ candidate.name }}</th>
+ <td>{{ stats['candidate_voters'][candidate.name] }}</td>
+ <td>{{ candidate.vote_count / stats['candidate_voters'][candidate.name] }}</td>
+ </tr>
+ {% endfor %}
+ {% endif %}
+ </table>
+ </section>
{% endblock %}
commit 304b3ff19abd8805659277471f76cd6a6cbb10ac
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:08:31 2014 +0100
Add method in the Vote class to retrieve some stats on a specified elections
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index ef80530..88773ed 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -291,3 +291,51 @@ class Vote(BASE):
return query.count()
else:
return query.all()
+
+ @classmethod
+ def get_election_stats(cls, session, election_id):
+ """ Return a dictionnary containing some statistics about the
+ specified elections.
+ (Number of voters, number of votes per candidates, total number
+ of votes)
+ """
+
+ stats = {}
+
+ n_voters = session.query(
+ sa.func.distinct(cls.voter)
+ ).filter(
+ cls.election_id == election_id
+ ).count()
+ stats['n_voters'] = n_voters
+
+ n_votes = session.query(
+ cls
+ ).filter(
+ cls.election_id == election_id
+ ).count()
+ stats['n_votes'] = n_votes
+
+ election = session.query(
+ Election
+ ).filter(
+ Election.id == election_id
+ ).first()
+
+ candidate_voters = {}
+ cnt = 0
+ for cand in election.candidates:
+ cnt += 1
+ n_voters = session.query(
+ sa.func.distinct(cls.voter)
+ ).filter(
+ cls.election_id == election_id
+ ).filter(
+ cls.candidate_id == cand.id
+ ).count()
+ candidate_voters[cand.name] = n_voters
+
+ stats['candidate_voters'] = candidate_voters
+ stats['n_candidates'] = cnt
+
+ return stats
commit 2222c930c8b0b23cc37300e053f068b88374fd94
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:06:50 2014 +0100
The FAS client is FAS2 not fas2
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 844c78d..9d7d9e7 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -177,7 +177,7 @@ def about_election(election_alias):
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
+ FAS2.person_by_username(candidate.name)['human_name']
except (KeyError, AuthError):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
@@ -330,7 +330,7 @@ def vote_range(election_alias):
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
+ FAS2.person_by_username(candidate.name)['human_name']
except (KeyError, AuthError):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
@@ -401,7 +401,7 @@ def vote_simple(election_alias):
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
+ FAS2.person_by_username(candidate.name)['human_name']
except (KeyError, AuthError):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
@@ -697,7 +697,7 @@ def election_results(election_alias):
for candidate in election.candidates:
try:
usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
+ FAS2.person_by_username(candidate.name)['human_name']
except (KeyError, AuthError):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
commit e3bb36a76b640ccfe5a943f7f7c9e5cc3c7f37ae
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:05:46 2014 +0100
Small style change and add information message when displaying embargoed results
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index a0f226f..844c78d 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -664,15 +664,18 @@ def election_results(election_alias):
if not isinstance(election, models.Election):
return election
- elif election.embargoed == 1:
+ elif election.embargoed:
if not hasattr(flask.g, 'fas_user') or not flask.g.fas_user:
flask.flash("We are sorry. The results for this election"
- "cannot be viewed because they are currently"
- " embargoed pending formal announcement.")
+ "cannot be viewed because they are currently "
+ "embargoed pending formal announcement.")
return redirect.safe_redirect_back()
else:
if APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
flask.g.fas_user.groups:
+ flask.flash("The results for this election are currently "
+ "embargoed pending formal announcement.",
+ "error")
pass
else:
match = 0
@@ -685,8 +688,8 @@ def election_results(election_alias):
if match == 0:
flask.flash(
"We are sorry. The results for this election"
- "cannot be viewed because they are currently"
- " embargoed pending formal announcement.")
+ "cannot be viewed because they are currently "
+ "embargoed pending formal announcement.")
return redirect.safe_redirect_back()
usernamemap = {}
commit 5011fa7592734e338042d71c1370806b0639c815
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:04:18 2014 +0100
Mark with a red starts the field mandatory when creating an election
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 99ecf02..7220374 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -7,12 +7,12 @@ from fedora_elections.models import Election
class ElectionForm(wtf.Form):
summary = wtforms.TextField(
- 'Summary', [
+ 'Summary<span class="error">*</span>', [
wtforms.validators.Required(),
wtforms.validators.Length(max=150)])
alias = wtforms.TextField(
- 'Alias', [
+ 'Alias<span class="error">*</span>', [
wtforms.validators.Required(),
wtforms.validators.Length(max=100),
wtforms.validators.Regexp(
@@ -22,31 +22,31 @@ class ElectionForm(wtf.Form):
])
description = wtforms.TextAreaField(
- 'Description', [
+ 'Description<span class="error">*</span>', [
wtforms.validators.Required()])
voting_type = wtforms.RadioField(
- 'Type',
+ 'Type<span class="error">*</span>',
choices=[('range', 'Range Voting'),
('simple', 'Simple Voting')],
default='range')
url = wtforms.TextField(
- 'URL', [
+ 'URL<span class="error">*</span>', [
wtforms.validators.Required(),
wtforms.validators.URL(),
wtforms.validators.Length(max=250)])
start_date = wtforms.DateField(
- 'Start date', [
+ 'Start date<span class="error">*</span>', [
wtforms.validators.Required()])
end_date = wtforms.DateField(
- 'End date', [
+ 'End date<span class="error">*</span>', [
wtforms.validators.Required()])
number_elected = wtforms.IntegerField(
- 'Number elected', [
+ 'Number elected<span class="error">*</span>', [
wtforms.validators.Required(),
wtforms.validators.NumberRange(min=1)],
default=1)
commit c10145568e5f6c142f4310e16971621d62416fcb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:03:54 2014 +0100
When running elections2 locally, activate the debug mode
diff --git a/runserver.py b/runserver.py
index 293b6d0..f5ce1d9 100644
--- a/runserver.py
+++ b/runserver.py
@@ -3,4 +3,5 @@ __main__.__requires__ = ['SQLAlchemy >= 0.7']
import pkg_resources
from fedora_elections import APP
+APP.debug = True
APP.run()
commit 3351e2c4fa0a0483fdbd49c3528ad3720c7e7a67
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Apr 1 08:09:14 2014 +0200
Specify clearly that URL are optional when uploading multiple candidates
diff --git a/fedora_elections/templates/admin/candidate_multi.html b/fedora_elections/templates/admin/candidate_multi.html
index f1e4c3f..c8113a2 100644
--- a/fedora_elections/templates/admin/candidate_multi.html
+++ b/fedora_elections/templates/admin/candidate_multi.html
@@ -10,7 +10,7 @@
<p>
The string below needs to be formatted as:
- "Name1!URL1 (opt)|Name2!URL2 (opt)|Name3!URL3 (opt)"
+ "Name1!URL1 [optional]|Name2!URL2 [optional]|Name3!URL3 [optional]"
</p>
<form action="" method="post">
commit 08d67b4373e235065993e788de59d561458b4148
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Feb 20 12:04:07 2014 +0100
Add link on the admin page of an election to import multiple candidates
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index 2643297..a36e019 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -41,5 +41,9 @@
<a href="{{ url_for('admin_add_candidate',
election_alias=election.alias) }}">
Add a candidate
+ </a> -
+ <a href="{{ url_for('admin_add_multi_candidate',
+ election_alias=election.alias) }}">
+ Add a multiple candidates
</a>
{% endblock %}
commit 317f201f19b4210f71260d8f59ced3073acb050f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Feb 20 12:03:19 2014 +0100
Add the controller to import multiple candidates in one go
Fixes https://github.com/fedora-infra/elections/issues/5
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index d4c66cb..a0f226f 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -81,6 +81,7 @@ def is_elections_admin(user):
return len(set(user.groups).intersection(admins)) > 0
+
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
@@ -131,6 +132,7 @@ def get_valid_election(election_alias, ended=False):
return election
+
@APP.context_processor
def inject_variables():
''' Inject a set of variable that we want for every pages (every
@@ -558,6 +560,48 @@ def admin_add_candidate(election_alias):
submit_text='Add candidate')
+(a)APP.route('/admin/<election_alias>/candidates/new/multi', methods=('GET', 'POST'))
+@election_admin_required
+def admin_add_multi_candidate(election_alias):
+ election = models.Election.get(SESSION, alias=election_alias)
+ if not election:
+ flask.abort(404)
+
+ form = forms.MultiCandidateForm()
+ if form.validate_on_submit():
+
+ candidates_name = []
+ for entry in form.candidate.data.strip().split("|"):
+ candidate = entry.split("!")
+ #No url
+ if len(candidate) == 1:
+ cand = models.Candidate(
+ election=election,
+ name=candidate[0])
+ SESSION.add(cand)
+ candidates_name.append(cand.name)
+ # With url
+ elif len(candidate) == 2:
+ cand = models.Candidate(
+ election=election,
+ name=candidate[0],
+ url=candidate[1])
+ SESSION.add(cand)
+ candidates_name.append(cand.name)
+ else:
+ flask.flash("There was an issue!")
+
+ SESSION.commit()
+ flask.flash('Added candidates: "%s"' % ', '.join(candidates_name))
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+
+ return flask.render_template(
+ 'admin/candidate_multi.html',
+ form=form,
+ submit_text='Add candidates')
+
+
@APP.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
methods=('GET', 'POST'))
@election_admin_required
commit c8aff74aae470bc9a37280a18eb6c3360662b395
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Feb 20 12:02:47 2014 +0100
Add the template to add multiple candidates to an election
diff --git a/fedora_elections/templates/admin/candidate_multi.html b/fedora_elections/templates/admin/candidate_multi.html
new file mode 100644
index 0000000..f1e4c3f
--- /dev/null
+++ b/fedora_elections/templates/admin/candidate_multi.html
@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field_in_row %}
+
+{% block title %}{{ submit_text }}{% endblock %}
+
+{%block tag %}admin{% endblock %}
+
+{% block body %}
+ <h2>{{ submit_text }}</h2>
+
+ <p>
+ The string below needs to be formatted as:
+ "Name1!URL1 (opt)|Name2!URL2 (opt)|Name3!URL3 (opt)"
+ </p>
+
+ <form action="" method="post">
+ <fieldset>
+ <table>
+ {{ render_field_in_row(form.candidate) }}
+ </table>
+ </fieldset>
+
+ <br class="clear">
+ <p class="buttons">
+ <input type="submit" class="submit positive" value="{{ submit_text }}">
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
commit f716a70855c73b571b0366a03830623ced5017a8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Feb 20 12:02:05 2014 +0100
Add form to add multiple candidates in one go
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index b96dcc8..99ecf02 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -88,5 +88,10 @@ class CandidateForm(wtf.Form):
url = wtforms.TextField('URL', [wtforms.validators.Length(max=250)])
+class MultiCandidateForm(wtf.Form):
+ candidate = wtforms.TextField(
+ 'Candidates', [wtforms.validators.Required()])
+
+
class ConfirmationForm(wtf.Form):
pass
commit 2c573ccb6a4d659f0695bbb4f0e644e7aab0bb4a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:57:26 2014 +0100
Update the template used as front page of the admin section
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index 47e1226..f27d9af 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -17,6 +17,13 @@
{% if elections %}
<table>
<tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ </tr>
+ {% for election in elections %}
+ <tr>
<td>
<a href="{{ url_for('admin_view_election',
election_alias=election.alias) }}">
@@ -27,18 +34,6 @@
<td>{{ election.end_date }}</td>
<td>{{ election.status }}</td>
</tr>
- {% for election in elections %}
- <tr>
- <td>
- <a href="{{ url_for('admin_view_election',
- election_alias=election.alias) }}">
- {{ election.summary }}
- </a>
- </td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- </tr>
{% endfor %}
</table>
{% else %}
commit 850349497c4eebf92bc8a9e8b50f38e582633c98
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:44:37 2014 +0100
Add a little text to the front page of elections
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index d4ddc43..ec0d4ca 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -42,6 +42,19 @@
{% block body %}
<h2>{{ title }}</h2>
+ {% if tag == 'index' %}
+ <p>
+ The <a href='https://fedoraproject.org'>Fedora project</a> is a community
+ driven project. It has a number of steering commities whose responsabilities
+ is to drive the project forward.
+ </p>
+ <p>
+ These committies are elected among volunteers of the community and by
+ the community members. The present application is meant to held these
+ elections.
+ </p>
+ {% endif %}
+
{% if cur_elections %}
{{ render_elections(cur_elections, 'Current elections') }}
{% endif %}
commit 6640a24c712e01c74c30d9091d240dc46d226c81
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:37:47 2014 +0100
Remove the frontpage boolean/checkbox from the form as we no longer use it
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 26d602c..b96dcc8 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -54,8 +54,6 @@ class ElectionForm(wtf.Form):
candidates_are_fasusers = wtforms.BooleanField(
'Candidates are FAS users?')
- frontpage = wtforms.BooleanField('Show on front page?')
-
embargoed = wtforms.BooleanField('Embargo results?', default=True)
def __init__(form, election_id=None, *args, **kwargs):
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index 9a3a861..9488276 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -20,7 +20,6 @@
{{ render_field_in_row(form.end_date, " at 23:59:59 UTC") }}
{{ render_field_in_row(form.number_elected) }}
{{ render_field_in_row(form.candidates_are_fasusers) }}
- {{ render_field_in_row(form.frontpage) }}
{{ render_field_in_row(form.embargoed) }}
</table>
</fieldset>
commit 7eb4ae0fb63d2a98e0abf9eee895ed43d1bd1a6f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:37:28 2014 +0100
Small css fix to give a little space around the tables
diff --git a/fedora_elections/static/elections.css b/fedora_elections/static/elections.css
index d39dd7b..5322b11 100644
--- a/fedora_elections/static/elections.css
+++ b/fedora_elections/static/elections.css
@@ -200,3 +200,7 @@ a:visited.large_button {
.cell a {
color: #193C6E;
}
+
+body#index h3 {
+ padding-top: 1em;
+}
commit 7970b403e8eed9e9924d5e2a96bada4443454074
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:37:16 2014 +0100
Change the front page to display some information about the current, future and past elections
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 096a1ea..d4c66cb 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -147,12 +147,17 @@ def inject_variables():
@APP.route('/')
def index():
- elections = models.Election.search(SESSION, frontpage=True)
- num_elections = len(elections)
+ now = datetime.utcnow()
+
+ prev_elections = models.Election.get_older_election(SESSION, now)[:5]
+ cur_elections = models.Election.get_open_election(SESSION, now)
+ next_elections = models.Election.get_next_election(SESSION, now)[:3]
+
return flask.render_template(
'list/index.html',
- elections=elections,
- num_elections=num_elections,
+ prev_elections=prev_elections,
+ cur_elections=cur_elections,
+ next_elections=next_elections,
tag='index',
title="Elections")
@@ -181,7 +186,7 @@ def about_election(election_alias):
usernamemap=usernamemap)
-(a)APP.route('/archive')
+(a)APP.route('/archives')
def archived_elections():
now = datetime.utcnow()
@@ -208,7 +213,7 @@ def open_elections():
return flask.render_template(
'list/index.html',
- elections=elections,
+ next_elections=elections,
tag='open',
title='Open Elections')
@@ -463,7 +468,7 @@ def admin_new_election():
end_date=form.end_date.data,
number_elected=form.number_elected.data,
embargoed=form.embargoed.data,
- frontpage=form.frontpage.data,
+ frontpage=True,
voting_type=form.voting_type.data,
candidates_are_fasusers=form.candidates_are_fasusers.data,
fas_user=flask.g.fas_user.username,
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 6f9a53f..d4ddc43 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -2,9 +2,10 @@
{%block tag %}{{tag}}{% endblock %}
-{% block body %}
- <h2>{{ title }}</h2>
- {% if elections %}
+{% macro render_elections(election_list, title) -%}
+
+ <h3>{{title}}</h3>
+
<table>
<tr>
<th>Election</th>
@@ -13,7 +14,7 @@
<th>Status</th>
<th></th>
</tr>
- {% for election in elections %}
+ {% for election in election_list %}
<tr>
<td>{{ election.summary }}</td>
<td>{{ election.start_date }}</td>
@@ -35,7 +36,22 @@
</tr>
{% endfor %}
</table>
- {% else %}
- <p> No elections </p>
+
+{% endmacro %}
+
+{% block body %}
+ <h2>{{ title }}</h2>
+
+ {% if cur_elections %}
+ {{ render_elections(cur_elections, 'Current elections') }}
+ {% endif %}
+
+ {% if next_elections %}
+ {{ render_elections(next_elections, 'Next %s elections' % (next_elections|length)) }}
{% endif %}
+
+ {% if prev_elections %}
+ {{ render_elections(prev_elections, 'Last %s elections' % (prev_elections|length)) }}
+ {% endif %}
+
{% endblock %}
commit 7efe5f61e1abee15b20493ce569afd7ff5206141
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:34:57 2014 +0100
Add tags to display the menu correctly
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 4907395..096a1ea 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -153,6 +153,7 @@ def index():
'list/index.html',
elections=elections,
num_elections=num_elections,
+ tag='index',
title="Elections")
@@ -208,6 +209,7 @@ def open_elections():
return flask.render_template(
'list/index.html',
elections=elections,
+ tag='open',
title='Open Elections')
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index 4eba66f..47e1226 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -1,6 +1,8 @@
{% extends "base.html" %}
{% block title %}Election administration{% endblock %}
+{%block tag %}admin{% endblock %}
+
{% block body %}
<h2>Election administration</h2>
diff --git a/fedora_elections/templates/admin/candidate.html b/fedora_elections/templates/admin/candidate.html
index 98e0db4..06264e7 100644
--- a/fedora_elections/templates/admin/candidate.html
+++ b/fedora_elections/templates/admin/candidate.html
@@ -1,6 +1,10 @@
{% extends "base.html" %}
{% from "_formhelpers.html" import render_field_in_row %}
+
{% block title %}{{ submit_text }}{% endblock %}
+
+{%block tag %}admin{% endblock %}
+
{% block body %}
<h2>{{ submit_text }}</h2>
diff --git a/fedora_elections/templates/admin/delete_candidate.html b/fedora_elections/templates/admin/delete_candidate.html
index d764f19..26884e8 100644
--- a/fedora_elections/templates/admin/delete_candidate.html
+++ b/fedora_elections/templates/admin/delete_candidate.html
@@ -1,6 +1,10 @@
{% extends "base.html" %}
{% from "_formhelpers.html" import render_field %}
+
{% block title %}Delete candidate{% endblock %}
+
+{%block tag %}admin{% endblock %}
+
{% block body %}
<h2>{{ self.title() }}</h2>
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index a267f68..9a3a861 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -1,6 +1,10 @@
{% extends "base.html" %}
{% from "_formhelpers.html" import render_field_in_row %}
+
{% block title %}{{ submit_text }}{% endblock %}
+
+{%block tag %}admin{% endblock %}
+
{% block body %}
<h2>{{ self.title() }}</h2>
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index ed070f3..2643297 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -1,4 +1,7 @@
{% extends "base.html" %}
+
+{%block tag %}admin{% endblock %}
+
{% block body %}
<h2>{{ election.summary }}</h2>
<p>{{ election.description }}</p>
diff --git a/fedora_elections/templates/election/archive.html b/fedora_elections/templates/election/archive.html
index 9874dd5..f0d539a 100644
--- a/fedora_elections/templates/election/archive.html
+++ b/fedora_elections/templates/election/archive.html
@@ -1,4 +1,7 @@
{% extends "base.html" %}
+
+{%block tag %}archives{% endblock %}
+
{% block body %}
<h2>Past Elections</h2>
<table border="1" cellpadding="1">
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 425d70f..6f9a53f 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -1,4 +1,7 @@
{% extends "base.html" %}
+
+{%block tag %}{{tag}}{% endblock %}
+
{% block body %}
<h2>{{ title }}</h2>
{% if elections %}
commit fa5ee50cfa0b57e802b9b98ac7b281bc9a1603e3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:32:40 2014 +0100
Fix the Election.get_open_election method and add Election.get_next_election
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 113d814..ef80530 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -161,12 +161,14 @@ class Election(BASE):
@classmethod
def get_open_election(cls, session, limit):
- """ Return all the election which end_date if greater or equal to
- the provided limit.
+ """ Return all the election which start_date is lower and the
+ end_date if greater or equal to the provided limit.
"""
query = session.query(
cls
).filter(
+ cls.start_date < limit
+ ).filter(
cls.end_date >= limit
).order_by(
cls.start_date
@@ -174,6 +176,21 @@ class Election(BASE):
return query.all()
+ @classmethod
+ def get_next_election(cls, session, limit):
+ """ Return all the future elections whose start_date is greater than
+ the provided limit.
+ """
+ query = session.query(
+ cls
+ ).filter(
+ cls.start_date > limit
+ ).order_by(
+ cls.start_date
+ )
+
+ return query.all()
+
class ElectionAdminGroup(BASE):
__tablename__ = 'electionadmingroups'
commit 01bc953a73c21faeaeff3ca729fd10557bbd2711
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:57:26 2014 +0100
Update the template used as front page of the admin section
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index 1518ff8..4eba66f 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -15,10 +15,15 @@
{% if elections %}
<table>
<tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
+ <td>
+ <a href="{{ url_for('admin_view_election',
+ election_alias=election.alias) }}">
+ {{ election.summary }}
+ </a>
+ </td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
</tr>
{% for election in elections %}
<tr>
commit e86d8ef339eda7ed6667badd39216c0b76dcb171
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:18:55 2014 +0100
Rework the add election form
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index a409d2a..a267f68 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -20,67 +20,6 @@
{{ render_field_in_row(form.embargoed) }}
</table>
</fieldset>
- <div class="help">
- This is the name of the election that users will first see.</br>
- Example: <kbd>Fedora Board, November 2010</kbd>
- </div>
- <fieldset>
- {{ render_field(form.alias) }}
- </fieldset>
- <div class="help">
- A short, unique name that will be used for the election's URL.</br>
- Lowercase letters, numbers, hyphens and underscores only.</br>
- Example: <kbd>boardf15</kbd>
- </div>
- <fieldset>
- {{ render_field(form.description) }}
- </fieldset>
- <fieldset>
- <div class="row">
- {{ form.voting_type.label }}
- {% for subtype in form.voting_type %}
- {{ subtype }} {{ subtype.label }}
- {% endfor %}
- </div>
- </fieldset>
- <div class="help">
- Select the election type.
- </div>
- <fieldset>
- {{ render_field(form.url) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.start_date, " at 00:00:00 UTC") }}
- {{ render_field(form.end_date, " at 23:59:59 UTC") }}
- </fieldset>
- <div class="help">
- Enter dates in <kbd>YYYY-MM-DD</kbd> format.
- </div>
- <fieldset>
- {{ render_field(form.number_elected) }}
- </fieldset>
- <div class="help">
- This determines the cutoff for how many candidates win the election.
- For example, if you are electing four empty seats on a committee,
- enter <kbd>4</kbd> here.
- </div>
- <fieldset>
- {{ render_field(form.candidates_are_fasusers) }}
- </fieldset>
- <div class="help">
- Check this if every candidate in this election has a FAS user account.
- This will automatically pull full names and wiki page URLs from FAS.
- </div>
- <fieldset>
- {{ render_field(form.frontpage) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.embargoed) }}
- </fieldset>
- <div class="help">
- Embargo results when the election is finished. To unembargo them, edit
- the election and uncheck this box.
- </div>
<br class="clear">
<p class="buttons">
commit c2c08853f18669e8ad7b471824076fc540f78212
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:54:21 2014 +0100
Remove images we will no longer use
diff --git a/fedora_elections/images/border-left.png b/fedora_elections/images/border-left.png
deleted file mode 100644
index 6f256df..0000000
Binary files a/fedora_elections/images/border-left.png and /dev/null differ
diff --git a/fedora_elections/images/border-right.png b/fedora_elections/images/border-right.png
deleted file mode 100644
index 0e7365b..0000000
Binary files a/fedora_elections/images/border-right.png and /dev/null differ
diff --git a/fedora_elections/images/fedora-logo.png b/fedora_elections/images/fedora-logo.png
deleted file mode 100644
index ce95893..0000000
Binary files a/fedora_elections/images/fedora-logo.png and /dev/null differ
diff --git a/fedora_elections/images/line.png b/fedora_elections/images/line.png
deleted file mode 100644
index 3498cb9..0000000
Binary files a/fedora_elections/images/line.png and /dev/null differ
commit 9b4670bb847595a821236098b3dfd31a14522533
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:41:47 2014 +0100
CSS fixes: bring back the coloring when validating a vote - remove unused css
diff --git a/fedora_elections/static/elections.css b/fedora_elections/static/elections.css
index 5fcb352..d39dd7b 100644
--- a/fedora_elections/static/elections.css
+++ b/fedora_elections/static/elections.css
@@ -111,28 +111,60 @@ body#admin table {
padding: 0.6ex;
}
-#legend {
- margin-top: 4em;
- font-size: 85%;
-}
-
-#legend td {
- padding-left: 0.5em;
-}
-
-#legend td img {
- padding-right: 0.5em;
-}
-#legend th {
- min-width: 0px;
- padding-left: 0.5em;
+.vthot
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #9C0000;
+}
+
+.vtwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #FFA300;
+}
+
+.vtmedwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #93FF63;
+}
+
+.vtmedcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #00DAFE;
+}
+
+.vtcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #0033FF;
+}
+
+.vtcold
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #000083;
}
-
-#pending th {
- text-align: left;
-}
-
.italic {
font-style: italic;
}
commit a367a351acf4b210fbfc491fb0adbc1f115b2181
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:18:55 2014 +0100
Rework the add election form
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index e46bb3a..a409d2a 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -1,12 +1,24 @@
{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
+{% from "_formhelpers.html" import render_field_in_row %}
{% block title %}{{ submit_text }}{% endblock %}
{% block body %}
<h2>{{ self.title() }}</h2>
<form action="" method="post">
<fieldset>
- {{ render_field(form.summary) }}
+ <table>
+ {{ render_field_in_row(form.summary) }}
+ {{ render_field_in_row(form.alias) }}
+ {{ render_field_in_row(form.description) }}
+ {{ render_field_in_row(form.voting_type) }}
+ {{ render_field_in_row(form.url) }}
+ {{ render_field_in_row(form.start_date, " at 00:00:00 UTC") }}
+ {{ render_field_in_row(form.end_date, " at 23:59:59 UTC") }}
+ {{ render_field_in_row(form.number_elected) }}
+ {{ render_field_in_row(form.candidates_are_fasusers) }}
+ {{ render_field_in_row(form.frontpage) }}
+ {{ render_field_in_row(form.embargoed) }}
+ </table>
</fieldset>
<div class="help">
This is the name of the election that users will first see.</br>
@@ -71,10 +83,26 @@
</div>
<br class="clear">
- {% block election_form_extra %}{% endblock %}
<p class="buttons">
<input type="submit" class="submit positive" value="Save Changes">
</p>
{{ form.csrf_token }}
</form>
{% endblock %}
+
+{% block jscripts %}
+{{ super() }}
+<script type="text/javascript" src="{{ url_for('static', filename='jquery-1.10.2.min.js') }}"> </script>
+<script type="text/javascript" src="{{ url_for('static', filename='jquery-ui-1.9.2.custom.min.js') }}"> </script>
+
+<script type="text/javascript">
+ $(function(){
+ $('#start_date').datepicker({
+ dateFormat: "yy-mm-dd",
+ });
+ $('#end_date').datepicker({
+ dateFormat: "yy-mm-dd",
+ });
+ });
+</script>
+{% endblock %}
commit c557aea948cecf64fec03435a15e135425205f8d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:14:45 2014 +0100
Fix the page for adding a new candidate
diff --git a/fedora_elections/templates/admin/candidate.html b/fedora_elections/templates/admin/candidate.html
index a7b2fda..98e0db4 100644
--- a/fedora_elections/templates/admin/candidate.html
+++ b/fedora_elections/templates/admin/candidate.html
@@ -1,13 +1,15 @@
{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
+{% from "_formhelpers.html" import render_field_in_row %}
{% block title %}{{ submit_text }}{% endblock %}
{% block body %}
<h2>{{ submit_text }}</h2>
<form action="" method="post">
<fieldset>
- {{ render_field(form.name) }}
- {{ render_field(form.url) }}
+ <table>
+ {{ render_field_in_row(form.name) }}
+ {{ render_field_in_row(form.url) }}
+ </table>
</fieldset>
<br class="clear">
commit 33febfa9b30816b610d896d34d18a41741391f78
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:57:50 2014 +0100
Add dedicated macro to present the fields of a form in row
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
index ff781b7..e11eb67 100644
--- a/fedora_elections/templates/_formhelpers.html
+++ b/fedora_elections/templates/_formhelpers.html
@@ -10,4 +10,13 @@
{% endif %}
</div>
{% endmacro %}
-{# vim: set ts=2 sw=2 sta et: #}
+
+{% macro render_field_in_row(field, after="") %}
+<tr>
+ <td>{{ field.label }}</td>
+ <td>{{ field(**kwargs)|safe }}</td>
+{% if after %} <td>{{ after }}</td>{% endif %}
+{% if field.errors %}{% for error in field.errors
+%}<td class="error">{{ error }}</td>{% endfor %}{% endif %}
+</tr>
+{% endmacro %}
commit c94a845b90aaac59f2540192a7eba4ea62eb9a83
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:57:26 2014 +0100
Update the template used as front page of the admin section
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index fafefee..1518ff8 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -1,7 +1,18 @@
{% extends "base.html" %}
{% block title %}Election administration{% endblock %}
+
{% block body %}
<h2>Election administration</h2>
+
+ <ul>
+ <li>
+ <a href="{{ url_for('admin_new_election') }}">
+ Create new election
+ </a>
+ </li>
+ </ul>
+
+ {% if elections %}
<table>
<tr>
<th>Election</th>
@@ -23,4 +34,7 @@
</tr>
{% endfor %}
</table>
+ {% else %}
+ <p>No elections present in the database</p>
+ {% endif %}
{%endblock %}
commit e1a4431d24cd3030cc2f014bcccb07d84a631f3a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:57:10 2014 +0100
Inform the user if we have no election on the front page
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index b25a469..425d70f 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -1,6 +1,7 @@
{% extends "base.html" %}
{% block body %}
<h2>{{ title }}</h2>
+ {% if elections %}
<table>
<tr>
<th>Election</th>
@@ -31,4 +32,7 @@
</tr>
{% endfor %}
</table>
+ {% else %}
+ <p> No elections </p>
+ {% endif %}
{% endblock %}
commit f45807d655b88b028e0d98d444ede3ba2d0f48ab
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:56:56 2014 +0100
Update the base template to the new theme
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
index 47ab8bc..4dc54bb 100644
--- a/fedora_elections/templates/base.html
+++ b/fedora_elections/templates/base.html
@@ -1,134 +1,96 @@
<!DOCTYPE html>
-<html>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
<head>
- {% block head %}
- <title>
- Fedora Elections
- {% if self.title() %}
- —
- {% block title %}{% endblock %}
- {% endif %}
- </title>
- <link rel="stylesheet" type="text/css" media="screen"
- href="{{ url_for("static", filename="style.css") }}" />
- <link rel="stylesheet" type="text/css" media="screen"
- href="https://fedoraproject.org/static/css/fedora.css" />
- {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
- {% endfor %}
- {% endblock %}
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>{% block title %}Fedora supplementary wallpapers{% endblock %}</title>
+ {%block head %}{% endblock %}
+ <link rel="shortcut icon" type="image/vnd.microsoft.icon"
+ href="//fedoraproject.org/static/images/favicon.ico"/>
+ <link rel='stylesheet' type='text/css'
+ href="{{ url_for('static', filename='jquery-ui-1.9.2.custom.css') }}"/>
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="{{ url_for('static', filename='koji.css') }}"/>
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="{{ url_for('static', filename='elections.css') }}"/>
+ <link rel='stylesheet' type='text/css'
+ href='//fonts.googleapis.com/css?family=Comfortaa'/>
+
</head>
- <body>
- <div id="wrapper">
- <div id="head">
- <h1><a href="https://admin.fedoraproject.org/en/index">Fedora</a></h1>
- </div>
+ <body id="{%block tag %}{% endblock %}">
+
+ <div id="wrap">
+ <div id="innerwrap">
+
+ <!-- HEADER -->
+ <div id="header">
+ <img src="{{ url_for('static', filename='elections-logo.png') }}"
+ alt="Elections Logo" id="kojiLogo"/>
+ </div><!-- end header -->
- <div id="sidebar">
- <div id="nav">
- <h2>Navigation</h2>
+ <!-- MAIN NAVIGATION -->
+ <div id="mainNav">
+ <h4 class="hide">Main Site Links:</h4>
<ul>
- <li id="nav-home">
- <a href="http://fedoraproject.org/en/index">Home</a>
- </li>
- <li id="nav-get">
- <a href="http://fedoraproject.org/en/get-fedora">Get Fedora</a>
- </li>
- <li id="nav-join">
- <a href="http://fedoraproject.org/en/join-fedora">Join Fedora</a>
- </li>
- <li id="nav-help">
- <a href="http://fedoraproject.org/en/get-help">Get Help</a>
- </li>
+ <li id="indexTab"><a href="{{url_for('.index')}}">Home</a></li>
+ <li id="openTab"><a href="{{url_for('.open_elections')}}">Open</a></li>
+ <li id="archivesTab"><a href="{{url_for('.archived_elections')}}">Archives</a></li>
+ {% if is_admin %}
+ <li id="adminTab"><a href="{{url_for('.admin_view_elections')}}">Admin</a></li>
+ {% endif %}
</ul>
- </div>
- </div>
+ </div><!-- end mainNav -->
- <div id="content">
- <ul class="toolbar" id="#main-toolbar">
+ <span id="loginInfo">
{% if g.fas_user %}
- <li>
- Hello, {{ g.fas_user.username }} —
- <a href="{{ url_for('auth_logout') }}">log out</a>
- </li>
+ <span class="text">logged in as </span>
+ {{ g.fas_user.username }} |
+ <a href="{{ url_for('.auth_logout') }}">log out</a>
{% else %}
- <li>
- You are not logged in —
- <a href="{{ url_for('auth_login') }}">log in</a>
- </li>
+ <a href="{{ url_for('.auth_login') }}">login</a>
{% endif %}
- </ul>
- <h2>Elections</h2>
- <ul>
- <li>
- <a href="{{ url_for('open_elections') }}">
- Open elections
- </a>
- </li>
- <li>
- <a href="{{ url_for('archived_elections') }}">
- Archived elections
- </a>
- </li>
- </ul>
- <br>
- <h2>Administration</h2>
- <ul>
- <li>
- <a href="{{ url_for('admin_view_elections') }}">
- My elections
- </a>
- </li>
- <li>
- <a href="{{ url_for('admin_new_election') }}">
- Create new election
- </a>
- </li>
- </ul>
- {% with messages = get_flashed_messages() %}
- {% if messages %}
- <ul id="flashes">
- {% for message in messages %}
- <li>{{ message }}</li>
- {% endfor %}
- </ul>
- {% endif %}
- {% endwith %}
- {% block body %}
- {% endblock %}
- </div>
- </div>
- <div id="bottom">
- <div id="footer">
- {% block footer %}
- <p class="copy">
- Copyright © 2014 Red Hat, Inc. and others. Please send any
- comments or corrections to the
- <a href="mailto:webmaster@fedoraproject.org">
- websites team
- </a>.
- </p>
- <p class="disclaimer">
- The Fedora Project is maintained and driven by the community and
- sponsored by Red Hat. This is a community maintained site. Red
- Hat is not responsible for content.
- </p>
- <ul>
- <li class="first">
- <a href="/en/sponsors">Sponsors</a>
- </li>
- <li>
- <a href="http://fedoraproject.org/wiki/Legal">
- Legal
- </a>
- </li>
- <li>
- <a href="http://fedoraproject.org/wiki/Legal/TrademarkGuidelines">
- Trademark Guidelines
- </a>
- </li>
- </ul>
+ </span>
+
+ <div id="content">
+
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if category, messages %}
+ <ul id="flashes">
+ {% for category, message in messages %}
+ <li class="{{ category }}">{{ message |safe}}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {% endwith %}
+
+ {% block body %}{% endblock %}
+
</div>
- {% endblock %}
+
+ {% if options is defined %}
+ <div id="options">
+ <div id="options_title">
+ <h3>Options</h3>
+ </div>
+ <div id="delimiter"></div>
+ <div id="options_content">
+ {% block options %}{% endblock %}
+ </div>
+ </div>
+ {% endif %}
+
+ <p id="footer">
+ Copyright © 2013-2014 Red Hat
+ <a href="//github.com/fedora-infra/elections/"
+ target="_blank">elections</a>
+ -- version: {{ version }}
+ </p>
+
+ </div>
</div>
+
+ {% block jscripts %}
+ {% endblock %}
+
</body>
</html>
commit ef331f7e8a56de6df87ed739ee8902010fd35f14
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:55:58 2014 +0100
Add the koji.css our base and the elections.css dedicated to elections
diff --git a/fedora_elections/static/elections.css b/fedora_elections/static/elections.css
new file mode 100644
index 0000000..5fcb352
--- /dev/null
+++ b/fedora_elections/static/elections.css
@@ -0,0 +1,170 @@
+.message {
+ color: green;
+}
+
+.error {
+ color: red;
+}
+
+.warning {
+ color: #FF8000;
+}
+
+#galleria{
+ height: 250px;
+}
+
+.acls tr td {
+ vertical-align: text-top;
+}
+
+#header input {
+ border-radius: 10px;
+}
+
+h1, h2, h3 {
+ font-family: 'Comfortaa', cursive;
+}
+
+body#index #mainNav li#indexTab a,
+body#open #mainNav li#openTab a,
+body#archives #mainNav li#archivesTab a,
+body#admin #mainNav li#adminTab a {
+ background-color: #fff;
+ color: #444;
+ -webkit-border-top-left-radius: 5px;
+ -webkit-border-top-right-radius: 5px;
+ -moz-border-radius-topleft: 5px;
+ -moz-border-radius-topright: 5px;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+ min-width:0px;
+}
+
+body#admin table {
+ padding-top:2em;
+}
+
+#content {
+ float:left;
+ width:74%px;
+}
+
+#options {
+ float:right;
+ width:23%;
+ margin-right: 5px;
+ box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15) inset;
+ border-radius: 10px 10px 10px 10px;
+ position: absolute;
+ top:120px;
+ right:0;
+}
+
+#delimiter {
+ background-color:#193b6c;
+ height: 10px;
+}
+
+#options_title {
+ border-radius: 10px 10px 0px 0px;
+ padding: 1px 0em 1px 1em;
+ margin: 0px 0px 0px 0px;
+ background: #34609f;
+ color: #fff;
+}
+
+#options_content ul {
+ list-style-type: none;
+ margin-right: 5px;
+}
+
+#options_content li {
+ margin-bottom: 5px;
+}
+
+#options input[type="submit"] {
+ width: 90%;
+}
+
+/** Web App Styles **/
+/* Links to other web apps */
+#actions {
+ line-height: 1;
+ list-style: none;
+ margin: 1ex 0!important;
+ padding: 1ex 0ex 0ex 0ex;
+}
+
+#actions li {
+ display: inline;
+ margin-right: 1ex;
+}
+
+#metainfo th{
+ text-align: left;
+ background: none repeat scroll 0 0 #DDDDDD;
+}
+
+#metainfo th{
+ border: 1px solid #000000;
+ padding: 0.6ex;
+}
+
+#legend {
+ margin-top: 4em;
+ font-size: 85%;
+}
+
+#legend td {
+ padding-left: 0.5em;
+}
+
+#legend td img {
+ padding-right: 0.5em;
+}
+
+#legend th {
+ min-width: 0px;
+ padding-left: 0.5em;
+}
+
+#pending th {
+ text-align: left;
+}
+
+.italic {
+ font-style: italic;
+}
+
+.bold {
+ font-weight: bold;
+}
+
+.small {
+ font-size: 75%
+}
+
+.underline {
+ text-decoration: underline;
+}
+
+a.large_button,
+a:hover.large_button,
+a:visited.large_button {
+ color: white;
+}
+
+.large_button {
+ color: white;
+ background: #3C6EB4;
+ padding: 1em;
+ margin-bottom: 1em;
+ -moz-border-radius: 15px;
+ border-radius: 15px;
+ display:inline-block;
+}
+
+.cell a {
+ color: #193C6E;
+}
diff --git a/fedora_elections/static/header-bg.png b/fedora_elections/static/header-bg.png
new file mode 100644
index 0000000..f4c540a
Binary files /dev/null and b/fedora_elections/static/header-bg.png differ
diff --git a/fedora_elections/static/koji.css b/fedora_elections/static/koji.css
new file mode 100644
index 0000000..2636e83
--- /dev/null
+++ b/fedora_elections/static/koji.css
@@ -0,0 +1,501 @@
+/*
+ Koji styling - Updated for nuancier
+ Copyright (c) 2007,2008, 2009, 2012, 2013 Red Hat, Inc.
+
+ Authors:
+ Mike Bonnet <mikeb(a)redhat.com>
+ Mike McLean <mikem(a)redhat.com>
+ Dennis Gilmore <dgilmore(a)redhat.com>
+ Pierre-Yves Chibon <pingou(a)redhat.com>
+*/
+
+html {
+ min-width: 800px;
+}
+
+body {
+ margin: 0px;
+ padding: 0px;
+ font-size: small;
+ font-family: "Liberation Sans","Lucida Grande", "Luxi Sans", "Bitstream Vera Sans", helvetica, verdana, arial, sans-serif;
+ color: #666;
+ background: #fff url(header-bg.png) repeat-x;
+}
+
+a, a:visited, a:hover {
+ color: #0066CC;
+ text-decoration: none;
+}
+
+h4
+{
+ font-size:19px;
+ margin-bottom:14px;
+}
+
+#wrap {
+ min-width: 750px;
+ max-width:1100px;
+ margin-left:auto;
+ margin-right:auto;
+ padding: 0;
+ text-align: left;
+}
+
+#innerwrap {
+position:relative;
+}
+
+#header {
+height:87px
+}
+
+#headerSearch {
+ display:inline-block;
+ position:relative;
+ float:right;
+ top: 40px;
+}
+
+#headerSearch input,
+#headerSearch select {
+ font-size: 16px;
+}
+
+#headerSearch input[type="submit"], #pending input[type="submit"],
+.acls input[type="submit"], #options input[type="submit"]
+{
+ background-color: white;
+ box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15) inset;
+ border-radius: 10px 10px 10px 10px;
+ color: #3D69A8;
+}
+#headerSearch input[type="text"]
+{
+ border: 1px solid white;
+ box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15) inset;
+ -webkit-box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15) inset;
+ -moz-box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15) inset;
+ padding: 3px;
+}
+
+#kojiLogo {
+ /* Used only for the koji logo icon */
+ display:inline-block;
+ padding-top:8px;
+}
+
+#PoweredByKojiLogo {
+ /* Used only for the koji logo icon */
+ float: right;
+ border: 0px;
+ height: 20px;
+}
+
+
+div#content {
+ margin: 0 20px;
+ margin-top:30px;
+ clear: both;
+}
+
+p#footer {
+ padding-top: 40px;
+ margin-left: 15px;
+ line-height: 1.5em;
+ color: #999;
+ font-size: xx-small;
+ clear: both;
+}
+
+p#footer a {
+ text-decoration: none;
+}
+
+.hide {
+ display: none;
+}
+
+.disabled {
+ color: #808080;
+}
+
+#mainNav {
+ font-size:16px;
+ font-weight:bold;
+ height: 30px;
+}
+
+#mainNav ul {
+ padding: 0px;
+ margin: 0px;
+ list-style-type: none;
+}
+
+#mainNav ul li {
+
+ float: left;
+
+}
+
+#mainNav ul li a {
+ display: block;
+ color: rgba(255,255,255,0.8);
+ text-decoration: none;
+ padding: 0.5em 1.5em;
+ font-size: 0.77em;
+ height: 1.5em;
+}
+
+#mainNav ul li:hover {
+ -webkit-border-top-left-radius: 5px;
+ -webkit-border-top-right-radius: 5px;
+ -moz-border-radius-topleft: 5px;
+ -moz-border-radius-topright: 5px;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+ background-color:rgba(255,255,255,0.1);
+}
+
+body#summary #mainNav li#summaryTab a,
+body#tasks #mainNav li#tasksTab a,
+body#tags #mainNav li#tagsTab a,
+body#builds #mainNav li#buildsTab a,
+body#packages #mainNav li#packagesTab a,
+body#users #mainNav li#usersTab a,
+body#hosts #mainNav li#hostsTab a,
+body#buildtargets #mainNav li#buildtargetsTab a,
+body#reports #mainNav li#reportsTab a,
+body#search #mainNav li#searchTab a {
+ background-color: #fff;
+ color: #444;
+ -webkit-border-top-left-radius: 5px;
+ -webkit-border-top-right-radius: 5px;
+ -moz-border-radius-topleft: 5px;
+ -moz-border-radius-topright: 5px;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+}
+
+table {
+ border-spacing: 0px;
+}
+
+th {
+ font-weight: bold;
+ vertical-align: text-top;
+ /*min-width: 100px;*/
+ /*text-align:right;*/
+}
+
+td
+{
+ padding-left:15px;
+}
+
+th, td {
+ line-height:19px;
+}
+
+td.building {
+ color: #cc0;
+ width:45px;
+}
+
+td.complete {
+ color: #0c0;
+ width:45px;
+}
+
+td.deleted,
+td.failed,
+td.canceled {
+ color: #c00;
+ width:45px;
+}
+
+td.false {
+ color: #c00;
+}
+
+td.true {
+ color: #0c0;
+}
+
+img.sort {
+ /* used for up/down sort arrows*/
+ vertical-align: baseline;
+ width: 10px;
+ height: 9px;
+}
+
+td.paginate {
+ text-align: center;
+}
+
+form.pageJump {
+ float: right;
+ margin-left: 20px;
+}
+
+form.pageJump select {
+ font-size: smaller;
+}
+
+div.dataHeader {
+ font-weight: bold;
+ font-size:17px;
+ line-height:28px;
+ padding-bottom:4px;
+}
+
+div.pageHeader {
+ margin-bottom: 10px;
+ font-weight: bold;
+ font-size: 19px;
+}
+
+table.nested {
+ float: left;
+}
+
+td.container {
+ /*padding: 4px 0px;*/
+ width: 100%;
+}
+
+table.nested th,
+table.nested td {
+ padding: 2px 4px;
+}
+
+div.toggle {
+ padding: 6px;
+}
+
+td.tree {
+
+}
+
+.tree span.root {
+ font-weight: bold;
+
+}
+
+.tree ul {
+ padding-left: 2em;
+ list-style: none;
+ margin-top: 0em;
+ margin-bottom: 0em;
+}
+
+.tree span.treeBranch {
+ border-bottom: 1px solid #000;
+ border-left: 1px solid #000;
+ font-size: 1.2em;
+}
+
+.tree li.sibling > span.treeBranch {
+ border-left-width: 0em;
+}
+
+.tree li.sibling {
+ border-left: 1px solid #000;
+}
+
+.tree a {
+ text-decoration: none;
+}
+
+.tree span.treeLabel {
+ position: relative;
+ top: 0.6em;
+ margin-left: 1.2em;
+ padding-left: 0.2em;
+
+ font-size: 0.83em;
+}
+
+.tree > ul {
+ padding-bottom: 0.6em;
+}
+
+.hidden {
+ display: none;
+}
+
+.tree span.treeToggle {
+ font-weight: bold;
+}
+
+.tree span.treeLink {
+ font-size: smaller;
+}
+
+.adminLink {
+ color: #000;
+}
+
+img.stateimg {
+ margin-top: -6px;
+ margin-bottom: -6px;
+}
+
+.charlist {
+ text-align: center;
+}
+
+img.graphrow {
+ background-color: #00f;
+ vertical-align: bottom;
+}
+
+table.data-list {
+ width: 100%;
+}
+
+table.data-list td {
+ vertical-align: text-top;
+ padding-left:3px;
+}
+
+tr.list-header {
+ background-color: #fff;
+}
+
+tr.list-header th {
+ background-color: #eee;
+ box-shadow: 0 -1px 0 #ddd, 0 1px 0 #ddd;
+ text-align:left;
+ padding-left:4px;
+}
+
+tr.list-header th {
+min-width:1px;
+}
+
+table.nested th
+{
+ min-width:1px;
+}
+
+tr.row-even,
+tr.row-odd
+{
+ box-shadow: 0 1px 0 #ddd;
+}
+
+tr.row-even td,
+tr.row-odd td
+{
+padding-left:4px;
+}
+
+tr.row-odd td:first-child,
+tr.row-even td:first-child
+ {
+ box-shadow: -1px 0 0 #ddd;
+}
+
+tr.row-odd td:last-child,
+tr.row-even td:last-child {
+ box-shadow: 1px 0 0 #ddd;
+}
+
+tr.list-header th:first-child
+{
+ box-shadow: -1px 0 0 #ddd,0 -1px 0 #ddd, 0 1px 0 #ddd;
+}
+
+tr.list-header th:last-child
+{
+ box-shadow: 1px 0 0 #ddd,0 -1px 0 #ddd, 0 1px 0 #ddd;
+}
+
+tr.row-even td.tree {
+
+}
+
+tr.row-even td.tree span.treeLabel {
+
+}
+
+.taskfree, .taskfree:visited, .taskfree:hover {
+ color: #3300CC;
+ background-color:#fff;
+}
+.taskopen, .taskopen:visited, .taskopen:hover {
+ color: #FF6600;
+ background-color:#fff;
+}
+.taskclosed, .taskclosed:visited, .taskclosed:hover {
+ color: #00CC00;
+ background-color:#fff;
+}
+.taskcanceled, .taskcanceled:visited, .taskcanceled:hover {
+ color: #CC9900;
+ background-color:#fff;
+}
+.taskassigned, .taskassigned:visited, .taskassigned:hover {
+ color: #CC00FF;
+ background-color:#fff;
+}
+.taskfailed, .taskfailed:visited, .taskfailed:hover {
+ color: #CC0000;
+ background-color:#fff;
+}
+
+a.help {
+ text-decoration: underline;
+}
+
+abbr {
+ cursor: help;
+}
+
+.changelog {
+ font-family: monospace;
+ font-size: medium;
+ white-space: pre;
+}
+
+#headerHelp {
+ float: right;
+ margin: 15px 10px 0 0;
+}
+
+.filterlist {
+ font-size: smaller;
+}
+
+span#loginInfo {
+ background-color: #ccc;
+ font-weight: bold;
+ padding: 3px 15px;
+ position: absolute;
+ right: 0;
+ top: 0;
+ -webkit-border-bottom-left-radius: 3px;
+ -webkit-border-bottom-right-radius: 3px;
+ -moz-border-radius-bottomleft: 3px;
+ -moz-border-radius-bottomright: 3px;
+ border-bottom-left-radius: 3px;
+ border-bottom-right-radius: 3px;
+ box-shadow: 0 10px 10px rgba(255, 255, 255, 0.5) inset, 0 1px 3px rgba(0, 0, 0, 0.5);
+ -mox-box-shadow: 0 10px 10px rgba(255, 255, 255, 0.5) inset, 0 1px 3px rgba(0, 0, 0, 0.5);
+ -webkit-box-shadow: 0 10px 10px rgba(255, 255, 255, 0.5) inset, 0 1px 3px rgba(0, 0, 0, 0.5);
+}
+
+.smaller {
+ font-size: smaller;
+}
+
+.rpmheader {
+ /*font-family: monospace;
+ font-size: medium;
+ white-space: pre;*/
+}
+
+.error {
+ color: red;
+}
commit c711f8a1da50343a4daf7787eafc14e98b4040f2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:55:34 2014 +0100
Add jQuery CSS
diff --git a/fedora_elections/static/jquery-ui-1.9.2.custom.css b/fedora_elections/static/jquery-ui-1.9.2.custom.css
new file mode 100644
index 0000000..93bc89f
--- /dev/null
+++ b/fedora_elections/static/jquery-ui-1.9.2.custom.css
@@ -0,0 +1,416 @@
+/*! jQuery UI - v1.9.2 - 2012-11-26
+* http://jqueryui.com
+* Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.accordion.css, jquery.ui.button.css, jquery.ui.datepicker.css, jquery.ui.dialog.css, jquery.ui.menu.css, jquery.ui.spinner.css
+* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Helvetica%2CArial%2Csans-serif...
Highlight=55&borderColorHighlight=cccccc&fcHighlight=444444&iconColorHighlight=3c6eb4&bgColorError=ffffff&bgTextureError=01_flat.png&bgImgOpacityError=55&borderColorError=db3279&fcError=222222&iconColorError=db3279&bgColorOv%20%20%20%20erlay=eeeeee&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=80&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=60&thicknessShadow=4px&offsetTopShadow=-4px&offsetLeftShadow=-4px&c%20%20%20%20ornerRadiusShadow=0px
+* Copyright (c) 2012 jQuery Foundation and other contributors Licensed MIT */
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden { display: none; }
+.ui-helper-hidden-accessible { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
+.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
+.ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; }
+.ui-helper-clearfix:after { clear: both; }
+.ui-helper-clearfix { zoom: 1; }
+.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled { cursor: default !important; }
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
+.ui-resizable { position: relative;}
+.ui-resizable-handle { position: absolute;font-size: 0.1px; display: block; }
+.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
+.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
+.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
+.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }
+.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }
+.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
+.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
+.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
+.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}.ui-accordion .ui-accordion-header { display: block; cursor: pointer; position: relative; margin-top: 2px; padding: .5em .5em .5em .7em; zoom: 1; }
+.ui-accordion .ui-accordion-icons { padding-left: 2.2em; }
+.ui-accordion .ui-accordion-noicons { padding-left: .7em; }
+.ui-accordion .ui-accordion-icons .ui-accordion-icons { padding-left: 2.2em; }
+.ui-accordion .ui-accordion-header .ui-accordion-header-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
+.ui-accordion .ui-accordion-content { /*padding: 1em 2.2em; such a big padding cause issues on fedocal menu */ border-top: 0; overflow: auto; zoom: 1; }
+.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */
+.ui-button, .ui-button:link, .ui-button:visited, .ui-button:hover, .ui-button:active { text-decoration: none; }
+.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
+button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
+.ui-button-icons-only { width: 3.4em; }
+button.ui-button-icons-only { width: 3.7em; }
+
+/*button text element */
+.ui-button .ui-button-text { display: block; line-height: 1.4; }
+.ui-button-text-only .ui-button-text { padding: .4em 1em; }
+.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; }
+.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; }
+.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; }
+.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
+/* no icon support for input elements, provide padding by default */
+input.ui-button { padding: .4em 1em; }
+
+/*button icon element(s) */
+.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; }
+.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; }
+.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; }
+.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
+.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
+
+/*button sets*/
+.ui-buttonset { margin-right: 7px; }
+.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; }
+
+/* workarounds */
+button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
+.ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; }
+.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
+.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
+.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
+.ui-datepicker .ui-datepicker-prev { left:2px; }
+.ui-datepicker .ui-datepicker-next { right:2px; }
+.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
+.ui-datepicker .ui-datepicker-next-hover { right:1px; }
+.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; }
+.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
+.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; }
+.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
+.ui-datepicker select.ui-datepicker-month,
+.ui-datepicker select.ui-datepicker-year { width: 49%;}
+.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
+.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; }
+.ui-datepicker td { border: 0; padding: 1px; }
+.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
+.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; }
+.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi { width:auto; }
+.ui-datepicker-multi .ui-datepicker-group { float:left; }
+.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
+.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
+.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
+.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
+.ui-datepicker-row-break { clear:both; width:100%; font-size:0em; }
+
+/* RTL support */
+.ui-datepicker-rtl { direction: rtl; }
+.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+
+/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
+.ui-datepicker-cover {
+ position: absolute; /*must have*/
+ z-index: -1; /*must have*/
+ filter: mask(); /*must have*/
+ top: -4px; /*must have*/
+ left: -4px; /*must have*/
+ width: 200px; /*must have*/
+ height: 200px; /*must have*/
+}.ui-dialog { position: absolute; top: 0; left: 0; padding: .2em; width: 300px; overflow: hidden; }
+.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; }
+.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; }
+.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
+.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
+.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
+.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
+.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
+.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; }
+.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; }
+.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
+.ui-draggable .ui-dialog-titlebar { cursor: move; }
+.ui-menu { list-style:none; padding: 2px; margin: 0; display:block; outline: none; }
+.ui-menu .ui-menu { margin-top: -3px; position: absolute; }
+.ui-menu .ui-menu-item { margin: 0; padding: 0; zoom: 1; width: 100%; }
+.ui-menu .ui-menu-divider { margin: 5px -2px 5px -2px; height: 0; font-size: 0; line-height: 0; border-width: 1px 0 0 0; }
+.ui-menu .ui-menu-item a { text-decoration: none; display: block; padding: 2px .4em; line-height: 1.5; zoom: 1; font-weight: normal; }
+.ui-menu .ui-menu-item a.ui-state-focus,
+.ui-menu .ui-menu-item a.ui-state-active { font-weight: normal; margin: -1px; }
+
+.ui-menu .ui-state-disabled { font-weight: normal; margin: .4em 0 .2em; line-height: 1.5; }
+.ui-menu .ui-state-disabled a { cursor: default; }
+
+/* icon support */
+.ui-menu-icons { position: relative; }
+.ui-menu-icons .ui-menu-item a { position: relative; padding-left: 2em; }
+
+/* left-aligned */
+.ui-menu .ui-icon { position: absolute; top: .2em; left: .2em; }
+
+/* right-aligned */
+.ui-menu .ui-menu-icon { position: static; float: right; }
+.ui-spinner { position:relative; display: inline-block; overflow: hidden; padding: 0; vertical-align: middle; }
+.ui-spinner-input { border: none; background: none; padding: 0; margin: .2em 0; vertical-align: middle; margin-left: .4em; margin-right: 22px; }
+.ui-spinner-button { width: 16px; height: 50%; font-size: .5em; padding: 0; margin: 0; text-align: center; position: absolute; cursor: default; display: block; overflow: hidden; right: 0; }
+.ui-spinner a.ui-spinner-button { border-top: none; border-bottom: none; border-right: none; } /* more specificity required here to overide default borders */
+.ui-spinner .ui-icon { position: absolute; margin-top: -8px; top: 50%; left: 0; } /* vertical centre icon */
+.ui-spinner-up { top: 0; }
+.ui-spinner-down { bottom: 0; }
+
+/* TR overrides */
+.ui-spinner .ui-icon-triangle-1-s {
+ /* need to fix icons sprite */
+ background-position:-65px -16px;
+}
+
+/* Component containers
+----------------------------------*/
+.ui-widget { font-family: Helvetica,Arial,sans-serif; font-size: 1.1em; }
+.ui-widget .ui-widget { font-size: 1em; }
+.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Helvetica,Arial,sans-serif; font-size: 1em; }
+.ui-widget-content { border: 1px solid #dddddd; background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x; color: #444444; }
+/*.ui-widget-content a { color: #444444; }*/
+.ui-widget-header { border: 1px solid #dddddd; background: #dddddd url(images/ui-bg_highlight-soft_75_dddddd_1x100.png) 50% 50% repeat-x; color: #444444; font-weight: bold; }
+.ui-widget-header a { color: #444444; }
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #dddddd; background: #f6f6f6 url(images/ui-bg_highlight-soft_100_f6f6f6_1x100.png) 50% 50% repeat-x; font-weight: bold; color: #3c6eb4; }
+.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #3c6eb4; text-decoration: none; }
+.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #3c6eb 4; background: #3c6eb4 url(images/ui-bg_highlight-soft_25_3c6eb4_1x100.png) 50% 50% repeat-x; font-weight: bold; color: #ffffff; }
+.ui-state-hover a, .ui-state-hover a:hover, .ui-state-hover a:link, .ui-state-hover a:visited { color: #ffffff; text-decoration: none; }
+.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #dddddd; background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #e59728; }
+.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #e59728; text-decoration: none; }
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #cccccc; background: #ffffff url(images/ui-bg_flat_55_ffffff_40x100.png) 50% 50% repeat-x; color: #444444; }
+.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #444444; }
+.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #db3279; background: #ffffff url(images/ui-bg_flat_55_ffffff_40x100.png) 50% 50% repeat-x; color: #222222; }
+.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #222222; }
+.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #222222; }
+.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
+.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
+.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
+.ui-state-disabled .ui-icon { filter:Alpha(Opacity=35); } /* For IE8 - See #6059 */
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_db3279_256x240.png); }
+.ui-widget-content .ui-icon {background-image: url(images/ui-icons_db3279_256x240.png); }
+.ui-widget-header .ui-icon {background-image: url(images/ui-icons_3c6eb4_256x240.png); }
+.ui-state-default .ui-icon { background-image: url(images/ui-icons_666666_256x240.png); }
+.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); }
+.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); }
+.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_3c6eb4_256x240.png); }
+.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_db3279_256x240.png); }
+
+/* positioning */
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-on { background-position: -96px -144px; }
+.ui-icon-radio-off { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-start { background-position: -80px -160px; }
+/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 2px; -webkit-border-top-left-radius: 2px; -khtml-border-top-left-radius: 2px; border-top-left-radius: 2px; }
+.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 2px; -webkit-border-top-right-radius: 2px; -khtml-border-top-right-radius: 2px; border-top-right-radius: 2px; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 2px; -webkit-border-bottom-left-radius: 2px; -khtml-border-bottom-left-radius: 2px; border-bottom-left-radius: 2px; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 2px; -webkit-border-bottom-right-radius: 2px; -khtml-border-bottom-right-radius: 2px; border-bottom-right-radius: 2px; }
+
+/* Overlays */
+.ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .8;filter:Alpha(Opacity=80); }
+.ui-widget-shadow { margin: -4px 0 0 -4px; padding: 4px; background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .6;filter:Alpha(Opacity=60); -moz-border-radius: 8px; -khtml-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }
commit cce9fd261d148eb2ad265d98006d6eeaf3d1592f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:55:25 2014 +0100
Add jQuery and our jQuery UI
diff --git a/fedora_elections/static/jquery-1.10.2.min.js b/fedora_elections/static/jquery-1.10.2.min.js
new file mode 100644
index 0000000..da41706
--- /dev/null
+++ b/fedora_elections/static/jquery-1.10.2.min.js
@@ -0,0 +1,6 @@
+/*! jQuery v1.10.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license
+//@ sourceMappingURL=jquery-1.10.2.min.map
+*/
+(function(e,t){var n,r,i=typeof t,o=e.location,a=e.document,s=a.documentElement,l=e.jQuery,u=e.$,c={},p=[],f="1.10.2",d=p.concat,h=p.push,g=p.slice,m=p.indexOf,y=c.toString,v=c.hasOwnProperty,b=f.trim,x=function(e,t){return new x.fn.init(e,t,r)},w=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=/\S+/g,C=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)retu
rn this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:fun
ction(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expa
ndo:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)
for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){
t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];e
lse while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a
={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=st(),k=st(),E=st
(),S=!1,A=function(e,t){return e===t?(S=!0,0):0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp
("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.n
odeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=mt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+yt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,n,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function lt(e){return e[b]=!0,e}function ut(e)
{var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ct(e,t){var n=e.split("|"),r=e.length;while(r--)o.attrHandle[n[r]]=t}function pt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function dt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return lt(function(t){return t=+t,lt(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.defaultView;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.docume
ntElement,h=!s(n),i&&i.attachEvent&&i!==i.top&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),r.getElementsByTagName=ut(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ut(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ut(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TA
G=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=K.test(n.querySelectorAll))&&(ut(function(e){e.innerHTML="<select><option selected=''></option></select>",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ut(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=K.test(y=d.webkitMatchesSelector||d.mozM
atchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ut(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=K.test(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c
,e)-F.call(c,t):0;if(o===a)return pt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?pt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDupl
icates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:lt,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2]
;return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=mt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!
!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?lt(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call
(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:lt(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?lt(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:lt(function(e){return function(t){return at(e,t).length>0}}),contains:lt(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:lt(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:funct
ion(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for
(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},o.pseudos.nth=o.pseudos.eq;for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=ft(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=dt(n);function gt(){}gt.prototype=o.filters=o.pseudos,o.setFilters=new gt;function mt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function yt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function vt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;r
eturn t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function bt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function wt(e,t,n,r,i,o){return r&&!r[b]&&(r=wt(r)),i&&!i[b]&&(i=wt(i,o)),lt(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||Nt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:xt(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=xt(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y
=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=xt(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function Tt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=vt(function(e){return e===t},s,!0),p=vt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[vt(bt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return wt(l>1&&bt(f),l>1&&yt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&Tt(e.slice(l,r)),i>r&&Tt(e=e.slice(r)),i>r&&yt(e))}f.push(n)}return bt(f)}function Ct(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b])
;b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=xt(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?lt(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=mt(e)),n=t.length;while(n--)o=Tt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Ct(i,r))}return o};function Nt(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function kt(e,t,n,i){var a,s,u,c,p,f=mt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parent
Node||t))){if(s.splice(a,1),e=i.length&&yt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}r.sortStable=b.split("").sort(A).join("")===b,r.detectDuplicates=S,p(),r.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(f.createElement("div"))}),ut(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||ct("type|href|height|width",function(e,n,r){return r?t:e.getAttribute(n,"type"===n.toLowerCase()?1:2)}),r.attributes&&ut(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ct("value",function(e,n,r){return r||"input"!==e.nodeName.toLowerCase()?t:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||ct(B,function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&i.specified?i.value:e[n]===!0?n.toLowerCase():null}),x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXML
Doc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,
r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!l||i&&!u||(t=t||[],t=[e,t.slice?t.slice():t],n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o
){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement
("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav></:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("
value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;
left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="<table><tr><td></td><td>t</td></tr></table>",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),ty
peof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="<div></div>",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null)}),n=s=l=u=r=o=null,t
+}({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{ap
plet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(functi
on(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x
.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.acc
ess(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.clas
sName+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,r=0,o=x(this),a=e.match(T)||[];while(t=a[r++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this
).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0
)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFi
x[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocu
ment.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxL
ength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,a
rguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.
*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n
]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this
,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt
.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.
clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEv
entListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t)
{x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(th
is,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0==
=n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.
event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;f
or(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling"
)},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i
},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/<tbody/i,wt=/<|&#?\w+;/,Tt=/<(?:script|style|link)/i,Ct=/^(?:checkbox|radio)$/i,Nt=/checked\s*(?:[^=]|=\s*.checked.)/i,kt=/^$|
\/(?:java|ecma)script/i,Et=/^true\/(.*)/,St=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,At={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:x.support.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prep
end:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.acc
ess(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1></$2>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i
=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._dat
a(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"repl
aceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a
++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1></$2>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?"<table>"!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-
1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle);
+u[o]&&(delete u[o],c?delete n[l]:typeof n.removeAttribute!==i?n.removeAttribute(l):n[l]=null,p.push(o))}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}}),x.fn.extend({wrapAll:function(e){if(x.isFunction(e))return this.each(function(t){x(this).wrapAll(e.call(this,t))});if(this[0]){var t=x(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var P
t,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+w+")(.*)$","i"),Yt=RegExp("^("+w+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+w+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=x._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=x._data(r,"olddisplay",ln(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&x._data(r,"olddisplay",i?n:x.css(r,"display"))));for(a=0;s>a;a+
+)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}x.fn.extend({css:function(e,n){return x.access(this,function(e,n,r){var i,o,a={},s=0;if(x.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=x.css(e,n[s],!1,o);return a}return r!==t?x.style(e,n,r):x.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){nn(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":x.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,l=x.camelCase(n),u=e.style;if(n=x.cssProps[l]||(x.cssProps[l]=tn
(u,l)),s=x.cssHooks[n]||x.cssHooks[l],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:u[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(x.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||x.cssNumber[l]||(r+="px"),x.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(u[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{u[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,l=x.camelCase(n);return n=x.cssProps[l]||(x.cssProps[l]=tn(e.style,l)),s=x.cssHooks[n]||x.cssHooks[l],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||x.isNumeric(o)?o||0:a):a}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s.getPropertyValue(n)||s[n]:t,u=e.style;return s&&(""!==l||x.contains(e.ownerDocument,e)||(l=x.style(e,n)),Yt.test(l)&&Ut.test(n)&&(i=u.width,o=u.minWidth,a=u.maxWidth,u.minWidth=u.max
Width=u.width=l,l=s.width,u.width=i,u.minWidth=o,u.maxWidth=a)),l}):a.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s[n]:t,u=e.style;return null==l&&u&&u[n]&&(l=u[n]),Yt.test(l)&&!zt.test(n)&&(i=u.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),u.left="fontSize"===n?"1em":l,l=u.pixelLeft+"px",u.left=i,a&&(o.left=a)),""===l?"auto":l});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=x.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=x.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=x.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=x.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=x.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,
o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function ln(e){var t=a,n=Gt[e];return n||(n=un(e,t),"none"!==n&&n||(Pt=(Pt||x("<iframe frameborder='0' width='0' height='0'/>").css("cssText","display:block !important")).appendTo(t.documentElement),t=(Pt[0].contentWindow||Pt[0].contentDocument).document,t.write("<!doctype html><html><body>"),t.close(),n=un(e,t),Pt.detach()),Gt[e]=n),n}function un(e,t){var n=x(t.createElement(e)).appendTo(t.body),r=x.css(n[0],"display");return n.remove(),r}x.each(["height","width"],function(e,n){x.cssHooks[n]={get:function(e,r,i){return r?0===e.offsetWidth&&Xt.test(x.css(e,"display"))?x.swap(e,Qt,function(){return sn(e,n,i)}):sn(e,n,i):t},set:function(e,t,r){var i=r&&Rt(e);return on(e,t,r?an(e,n,r,x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,i),i):0)}}}),x.support.opacity|
|(x.cssHooks.opacity={get:function(e,t){return It.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=x.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===x.trim(o.replace($t,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=$t.test(o)?o.replace($t,i):o+" "+i)}}),x(function(){x.support.reliableMarginRight||(x.cssHooks.marginRight={get:function(e,n){return n?x.swap(e,{display:"inline-block"},Wt,[e,"marginRight"]):t}}),!x.support.pixelPosition&&x.fn.position&&x.each(["top","left"],function(e,n){x.cssHooks[n]={get:function(e,r){return r?(r=Wt(e,n),Yt.test(r)?x(e).position()[n]+"px":r):t}}})}),x.expr&&x.expr.filters&&(x.expr.filters.hidden=function(e){return 0>=e.offsetWidth&&0>=e.offsetHeight||!x.support.reliableHiddenOffsets&&"none"===(e.style&&e.style.display||x.css(e,"display"))},x.expr
.filters.visible=function(e){return!x.expr.filters.hidden(e)}),x.each({margin:"",padding:"",border:"Width"},function(e,t){x.cssHooks[e+t]={expand:function(n){var r=0,i={},o="string"==typeof n?n.split(" "):[n];for(;4>r;r++)i[e+Zt[r]+t]=o[r]||o[r-2]||o[0];return i}},Ut.test(e)||(x.cssHooks[e+t].set=on)});var cn=/%20/g,pn=/\[\]$/,fn=/\r?\n/g,dn=/^(?:submit|button|image|reset|file)$/i,hn=/^(?:input|select|textarea|keygen)/i;x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=x.prop(this,"elements");return e?x.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!x(this).is(":disabled")&&hn.test(this.nodeName)&&!dn.test(e)&&(this.checked||!Ct.test(e))}).map(function(e,t){var n=x(this).val();return null==n?null:x.isArray(n)?x.map(n,function(e){return{name:t.name,value:e.replace(fn,"\r\n")}}):{name:t.name,value:n.replace(fn,"\r\n")}}).get()}}),x.param=function(e,n){var r,i=[],o=functi
on(e,t){t=x.isFunction(t)?t():null==t?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(n===t&&(n=x.ajaxSettings&&x.ajaxSettings.traditional),x.isArray(e)||e.jquery&&!x.isPlainObject(e))x.each(e,function(){o(this.name,this.value)});else for(r in e)gn(r,e[r],n,o);return i.join("&").replace(cn,"+")};function gn(e,t,n,r){var i;if(x.isArray(t))x.each(t,function(t,i){n||pn.test(e)?r(e,i):gn(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==x.type(t))r(e,t);else for(i in t)gn(e+"["+i+"]",t[i],n,r)}x.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){x.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),x.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:fu
nction(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var mn,yn,vn=x.now(),bn=/\?/,xn=/#.*$/,wn=/([?&])_=[^&]*/,Tn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Cn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Nn=/^(?:GET|HEAD)$/,kn=/^\/\//,En=/^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,Sn=x.fn.load,An={},jn={},Dn="*/".concat("*");try{yn=o.href}catch(Ln){yn=a.createElement("a"),yn.href="",yn=yn.href}mn=En.exec(yn.toLowerCase())||[];function Hn(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(T)||[];if(x.isFunction(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function qn(e,n,r,i){var o={},a=e===jn;function s(l){var u;return o[l]=!0,x.each(e[l]||[],function(e,l){var c=l(n,r,i);return"string"!=typeof c||a||o[c]?a?!(u=c):t:(n.dataTypes.unshift(c),s(c
),!1)}),u}return s(n.dataTypes[0])||!o["*"]&&s("*")}function _n(e,n){var r,i,o=x.ajaxSettings.flatOptions||{};for(i in n)n[i]!==t&&((o[i]?e:r||(r={}))[i]=n[i]);return r&&x.extend(!0,e,r),e}x.fn.load=function(e,n,r){if("string"!=typeof e&&Sn)return Sn.apply(this,arguments);var i,o,a,s=this,l=e.indexOf(" ");return l>=0&&(i=e.slice(l,e.length),e=e.slice(0,l)),x.isFunction(n)?(r=n,n=t):n&&"object"==typeof n&&(a="POST"),s.length>0&&x.ajax({url:e,type:a,dataType:"html",data:n}).done(function(e){o=arguments,s.html(i?x("<div>").append(x.parseHTML(e)).find(i):e)}).complete(r&&function(e,t){s.each(r,o||[e.responseText,t,e])}),this},x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){x.fn[t]=function(e){return this.on(t,e)}}),x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:yn,type:"GET",isLocal:Cn.test(mn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Dn,text:"t
ext/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":x.parseJSON,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?_n(_n(e,x.ajaxSettings),t):_n(x.ajaxSettings,e)},ajaxPrefilter:Hn(An),ajaxTransport:Hn(jn),ajax:function(e,n){"object"==typeof e&&(n=e,e=t),n=n||{};var r,i,o,a,s,l,u,c,p=x.ajaxSetup({},n),f=p.context||p,d=p.context&&(f.nodeType||f.jquery)?x(f):x.event,h=x.Deferred(),g=x.Callbacks("once memory"),m=p.statusCode||{},y={},v={},b=0,w="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c){c={};while(t=Tn.exec(a))c[t[1].toLowerCase()]=t[2]}t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase(
);return b||(e=v[n]=v[n]||e,y[e]=t),this},overrideMimeType:function(e){return b||(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)m[t]=[m[t],e[t]];else C.always(e[C.status]);return this},abort:function(e){var t=e||w;return u&&u.abort(t),k(0,t),this}};if(h.promise(C).complete=g.add,C.success=C.done,C.error=C.fail,p.url=((e||p.url||yn)+"").replace(xn,"").replace(kn,mn[1]+"//"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=x.trim(p.dataType||"*").toLowerCase().match(T)||[""],null==p.crossDomain&&(r=En.exec(p.url.toLowerCase()),p.crossDomain=!(!r||r[1]===mn[1]&&r[2]===mn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(mn[3]||("http:"===mn[1]?"80":"443")))),p.data&&p.processData&&"string"!=typeof p.data&&(p.data=x.param(p.data,p.traditional)),qn(An,p,n,C),2===b)return C;l=p.global,l&&0===x.active++&&x.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Nn.test(p.type),o=p.url,p.hasContent||(p.data&&(o=p.url+=(bn.test(o)?"&":"?")+p.data,
delete p.data),p.cache===!1&&(p.url=wn.test(o)?o.replace(wn,"$1_="+vn++):o+(bn.test(o)?"&":"?")+"_="+vn++)),p.ifModified&&(x.lastModified[o]&&C.setRequestHeader("If-Modified-Since",x.lastModified[o]),x.etag[o]&&C.setRequestHeader("If-None-Match",x.etag[o])),(p.data&&p.hasContent&&p.contentType!==!1||n.contentType)&&C.setRequestHeader("Content-Type",p.contentType),C.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Dn+"; q=0.01":""):p.accepts["*"]);for(i in p.headers)C.setRequestHeader(i,p.headers[i]);if(p.beforeSend&&(p.beforeSend.call(f,C,p)===!1||2===b))return C.abort();w="abort";for(i in{success:1,error:1,complete:1})C[i](p[i]);if(u=qn(jn,p,n,C)){C.readyState=1,l&&d.trigger("ajaxSend",[C,p]),p.async&&p.timeout>0&&(s=setTimeout(function(){C.abort("timeout")},p.timeout));try{b=1,u.send(y,k)}catch(N){if(!(2>b))throw N;k(-1,N)}}else k(-1,"No Transport");function k(e,n,r,i){var c,y,v,w,T,N=n;2!==b&&(b=2,s&&
clearTimeout(s),u=t,a=i||"",C.readyState=e>0?4:0,c=e>=200&&300>e||304===e,r&&(w=Mn(p,C,r)),w=On(p,w,C,c),c?(p.ifModified&&(T=C.getResponseHeader("Last-Modified"),T&&(x.lastModified[o]=T),T=C.getResponseHeader("etag"),T&&(x.etag[o]=T)),204===e||"HEAD"===p.type?N="nocontent":304===e?N="notmodified":(N=w.state,y=w.data,v=w.error,c=!v)):(v=N,(e||!N)&&(N="error",0>e&&(e=0))),C.status=e,C.statusText=(n||N)+"",c?h.resolveWith(f,[y,N,C]):h.rejectWith(f,[C,N,v]),C.statusCode(m),m=t,l&&d.trigger(c?"ajaxSuccess":"ajaxError",[C,p,c?y:v]),g.fireWith(f,[C,N]),l&&(d.trigger("ajaxComplete",[C,p]),--x.active||x.event.trigger("ajaxStop")))}return C},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,n){return x.get(e,t,n,"script")}}),x.each(["get","post"],function(e,n){x[n]=function(e,r,i,o){return x.isFunction(r)&&(o=o||i,i=r,r=t),x.ajax({url:e,type:n,dataType:o,data:r,success:i})}});function Mn(e,n,r){var i,o,a,s,l=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),o=
==t&&(o=e.mimeType||n.getResponseHeader("Content-Type"));if(o)for(s in l)if(l[s]&&l[s].test(o)){u.unshift(s);break}if(u[0]in r)a=u[0];else{for(s in r){if(!u[0]||e.converters[s+" "+u[0]]){a=s;break}i||(i=s)}a=a||i}return a?(a!==u[0]&&u.unshift(a),r[a]):t}function On(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(p){return{state:"parsererror",error:a?p:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-
ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),x.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=a.head||x("head")[0]||a.documentElement;return{send:function(t,i){n=a.createElement("script"),n.async=!0,e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,t){(t||!n.readyState||/loaded|complete/.test(n.readyState))&&(n.onload=n.onreadystatechange=null,n.parentNode&&n.parentNode.removeChild(n),n=null,t||i(200,"success"))},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(t,!0)}}}});var Fn=[],Bn=/(=)\?(?=&|$)|\?\?/;x.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Fn.pop()||x.expando+"_"+vn++;return this[e]=!0,e}}),x.ajaxPrefilter("json jsonp",function(n,r,i){var o,a,s,l=n.jsonp!==!1&&(Bn.test(n.url)?"url":"string"==typeof
n.data&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Bn.test(n.data)&&"data");return l||"jsonp"===n.dataTypes[0]?(o=n.jsonpCallback=x.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,l?n[l]=n[l].replace(Bn,"$1"+o):n.jsonp!==!1&&(n.url+=(bn.test(n.url)?"&":"?")+n.jsonp+"="+o),n.converters["script json"]=function(){return s||x.error(o+" was not called"),s[0]},n.dataTypes[0]="json",a=e[o],e[o]=function(){s=arguments},i.always(function(){e[o]=a,n[o]&&(n.jsonpCallback=r.jsonpCallback,Fn.push(o)),s&&x.isFunction(a)&&a(s[0]),s=a=t}),"script"):t});var Pn,Rn,Wn=0,$n=e.ActiveXObject&&function(){var e;for(e in Pn)Pn[e](t,!0)};function In(){try{return new e.XMLHttpRequest}catch(t){}}function zn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}x.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&In()||zn()}:In,Rn=x.ajaxSettings.xhr(),x.support.cors=!!Rn&&"withCredentials"in Rn,Rn=x.support.ajax=!!Rn,Rn&&x.ajaxTransport(func
tion(n){if(!n.crossDomain||x.support.cors){var r;return{send:function(i,o){var a,s,l=n.xhr();if(n.username?l.open(n.type,n.url,n.async,n.username,n.password):l.open(n.type,n.url,n.async),n.xhrFields)for(s in n.xhrFields)l[s]=n.xhrFields[s];n.mimeType&&l.overrideMimeType&&l.overrideMimeType(n.mimeType),n.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");try{for(s in i)l.setRequestHeader(s,i[s])}catch(u){}l.send(n.hasContent&&n.data||null),r=function(e,i){var s,u,c,p;try{if(r&&(i||4===l.readyState))if(r=t,a&&(l.onreadystatechange=x.noop,$n&&delete Pn[a]),i)4!==l.readyState&&l.abort();else{p={},s=l.status,u=l.getAllResponseHeaders(),"string"==typeof l.responseText&&(p.text=l.responseText);try{c=l.statusText}catch(f){c=""}s||!n.isLocal||n.crossDomain?1223===s&&(s=204):s=p.text?200:404}}catch(d){i||o(-1,d)}p&&o(s,c,p,u)},n.async?4===l.readyState?setTimeout(r):(a=++Wn,$n&&(Pn||(Pn={},x(e).unload($n)),Pn[a]=r),l.onreadystatechange=r):r()},abort:function()
{r&&r(t,!0)}}}});var Xn,Un,Vn=/^(?:toggle|show|hide)$/,Yn=RegExp("^(?:([+-])=|)("+w+")([a-z%]*)$","i"),Jn=/queueHooks$/,Gn=[nr],Qn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=Yn.exec(t),o=i&&i[3]||(x.cssNumber[e]?"":"px"),a=(x.cssNumber[e]||"px"!==o&&+r)&&Yn.exec(x.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,x.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};function Kn(){return setTimeout(function(){Xn=t}),Xn=x.now()}function Zn(e,t,n){var r,i=(Qn[t]||[]).concat(Qn["*"]),o=0,a=i.length;for(;a>o;o++)if(r=i[o].call(n,t,e))return r}function er(e,t,n){var r,i,o=0,a=Gn.length,s=x.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;var t=Xn||Kn(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;for(;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u
]),!1)},u=s.promise({elem:e,props:x.extend({},t),opts:x.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:Xn||Kn(),duration:n.duration,tweens:[],createTween:function(t,n){var r=x.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(tr(c,u.opts.specialEasing);a>o;o++)if(r=Gn[o].call(u,e,c,u.opts))return r;return x.map(c,Zn,u),x.isFunction(u.opts.start)&&u.opts.start.call(e,u),x.fx.timer(x.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function tr(e,t){var n,r,i,o,a;for(n in e)if(r=x.camelCase(n),i=t[r],o=e[n],x.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=x.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n
in e||(e[n]=o[n],t[n]=i)}else t[r]=i}x.Animation=x.extend(er,{tweener:function(e,t){x.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;i>r;r++)n=e[r],Qn[n]=Qn[n]||[],Qn[n].unshift(t)},prefilter:function(e,t){t?Gn.unshift(e):Gn.push(e)}});function nr(e,t,n){var r,i,o,a,s,l,u=this,c={},p=e.style,f=e.nodeType&&nn(e),d=x._data(e,"fxshow");n.queue||(s=x._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,u.always(function(){u.always(function(){s.unqueued--,x.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],"inline"===x.css(e,"display")&&"none"===x.css(e,"float")&&(x.support.inlineBlockNeedsLayout&&"inline"!==ln(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",x.support.shrinkWrapBlocks||u.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.ove
rflow[2]}));for(r in t)if(i=t[r],Vn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(f?"hide":"show"))continue;c[r]=d&&d[r]||x.style(e,r)}if(!x.isEmptyObject(c)){d?"hidden"in d&&(f=d.hidden):d=x._data(e,"fxshow",{}),o&&(d.hidden=!f),f?x(e).show():u.done(function(){x(e).hide()}),u.done(function(){var t;x._removeData(e,"fxshow");for(t in c)x.style(e,t,c[t])});for(r in c)a=Zn(f?d[r]:0,r,u),r in d||(d[r]=a.start,f&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function rr(e,t,n,r,i){return new rr.prototype.init(e,t,n,r,i)}x.Tween=rr,rr.prototype={constructor:rr,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(x.cssNumber[n]?"":"px")},cur:function(){var e=rr.propHooks[this.prop];return e&&e.get?e.get(this):rr.propHooks._default.get(this)},run:function(e){var t,n=rr.propHooks[this.prop];return this.pos=t=this.options.duration?x.easing[this.easing](e,this.options.duration*e,0,1,t
his.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):rr.propHooks._default.set(this),this}},rr.prototype.init.prototype=rr.prototype,rr.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=x.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){x.fx.step[e.prop]?x.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[x.cssProps[e.prop]]||x.cssHooks[e.prop])?x.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},rr.propHooks.scrollTop=rr.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},x.each(["toggle","show","hide"],function(e,t){var n=x.fn[t];x.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ir(t,!0),e,r,i)}}),x.fn.extend({fadeTo:function(e,t,n,r){return this.filter(nn).css("opacity",0).show()
.end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=x.isEmptyObject(e),o=x.speed(t,n,r),a=function(){var t=er(this,x.extend({},e),o);(i||x._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return"string"!=typeof e&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=null!=e&&e+"queueHooks",o=x.timers,a=x._data(this);if(n)a[n]&&a[n].stop&&i(a[n]);else for(n in a)a[n]&&a[n].stop&&Jn.test(n)&&i(a[n]);for(n=o.length;n--;)o[n].elem!==this||null!=e&&o[n].queue!==e||(o[n].anim.stop(r),t=!1,o.splice(n,1));(t||!r)&&x.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=x._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=x.timers,a=r?r.length:0;for(n.finish=!0,x.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0)
,o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}});function ir(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=Zt[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}x.each({slideDown:ir("show"),slideUp:ir("hide"),slideToggle:ir("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){x.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),x.speed=function(e,t,n){var r=e&&"object"==typeof e?x.extend({},e):{complete:n||!n&&t||x.isFunction(e)&&e,duration:e,easing:n&&t||t&&!x.isFunction(t)&&t};return r.duration=x.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in x.fx.speeds?x.fx.speeds[r.duration]:x.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){x.isFunction(r.old)&&r.old.call(this),r.queue&&x.dequeue(this,r.queue)},r},x.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.
PI)/2}},x.timers=[],x.fx=rr.prototype.init,x.fx.tick=function(){var e,n=x.timers,r=0;for(Xn=x.now();n.length>r;r++)e=n[r],e()||n[r]!==e||n.splice(r--,1);n.length||x.fx.stop(),Xn=t},x.fx.timer=function(e){e()&&x.timers.push(e)&&x.fx.start()},x.fx.interval=13,x.fx.start=function(){Un||(Un=setInterval(x.fx.tick,x.fx.interval))},x.fx.stop=function(){clearInterval(Un),Un=null},x.fx.speeds={slow:600,fast:200,_default:400},x.fx.step={},x.expr&&x.expr.filters&&(x.expr.filters.animated=function(e){return x.grep(x.timers,function(t){return e===t.elem}).length}),x.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){x.offset.setOffset(this,e,t)});var n,r,o={top:0,left:0},a=this[0],s=a&&a.ownerDocument;if(s)return n=s.documentElement,x.contains(n,a)?(typeof a.getBoundingClientRect!==i&&(o=a.getBoundingClientRect()),r=or(s),{top:o.top+(r.pageYOffset||n.scrollTop)-(n.clientTop||0),left:o.left+(r.pageXOffset||n.scrollLeft)-(n.clientLeft||0)}):o},x.offset={setOf
fset:function(e,t,n){var r=x.css(e,"position");"static"===r&&(e.style.position="relative");var i=x(e),o=i.offset(),a=x.css(e,"top"),s=x.css(e,"left"),l=("absolute"===r||"fixed"===r)&&x.inArray("auto",[a,s])>-1,u={},c={},p,f;l?(c=i.position(),p=c.top,f=c.left):(p=parseFloat(a)||0,f=parseFloat(s)||0),x.isFunction(t)&&(t=t.call(e,n,o)),null!=t.top&&(u.top=t.top-o.top+p),null!=t.left&&(u.left=t.left-o.left+f),"using"in t?t.using.call(e,u):i.css(u)}},x.fn.extend({position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===x.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),x.nodeName(e[0],"html")||(n=e.offset()),n.top+=x.css(e[0],"borderTopWidth",!0),n.left+=x.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-x.css(r,"marginTop",!0),left:t.left-n.left-x.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||s;while(e&&!x.nodeName(e,"html")&&"static"===x.css(e,"position"))e=e.of
fsetParent;return e||s})}}),x.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);x.fn[e]=function(i){return x.access(this,function(e,i,o){var a=or(e);return o===t?a?n in a?a[n]:a.document.documentElement[i]:e[i]:(a?a.scrollTo(r?x(a).scrollLeft():o,r?o:x(a).scrollTop()):e[i]=o,t)},e,i,arguments.length,null)}});function or(e){return x.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}x.each({Height:"height",Width:"width"},function(e,n){x.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){x.fn[i]=function(i,o){var a=arguments.length&&(r||"boolean"!=typeof i),s=r||(i===!0||o===!0?"margin":"border");return x.access(this,function(n,r,i){var o;return x.isWindow(n)?n.document.documentElement["client"+e]:9===n.nodeType?(o=n.documentElement,Math.max(n.body["scroll"+e],o["scroll"+e],n.body["offset"+e],o["offset"+e],o["client"+e])):i===t?x.css(n,r,s):x.style(n,r,i,s)},n,a?i:t,a,null)}})}),x.fn.size=function(){return this.length
},x.fn.andSelf=x.fn.addBack,"object"==typeof module&&module&&"object"==typeof module.exports?module.exports=x:(e.jQuery=e.$=x,"function"==typeof define&&define.amd&&define("jquery",[],function(){return x}))})(window);
diff --git a/fedora_elections/static/jquery-ui-1.9.2.custom.min.js b/fedora_elections/static/jquery-ui-1.9.2.custom.min.js
new file mode 100644
index 0000000..15c62cd
--- /dev/null
+++ b/fedora_elections/static/jquery-ui-1.9.2.custom.min.js
@@ -0,0 +1,6 @@
+/*! jQuery UI - v1.9.2 - 2012-11-26
+* http://jqueryui.com
+* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.position.js, jquery.ui.draggable.js, jquery.ui.resizable.js, jquery.ui.accordion.js, jquery.ui.button.js, jquery.ui.datepicker.js, jquery.ui.dialog.js, jquery.ui.menu.js, jquery.ui.spinner.js
+* Copyright (c) 2012 jQuery Foundation and other contributors Licensed MIT */
+
+(function(e,t){function i(t,n){var r,i,o,u=t.nodeName.toLowerCase();return"area"===u?(r=t.parentNode,i=r.name,!t.href||!i||r.nodeName.toLowerCase()!=="map"?!1:(o=e("img[usemap=#"+i+"]")[0],!!o&&s(o))):(/input|select|textarea|button|object/.test(u)?!t.disabled:"a"===u?t.href||n:n)&&s(t)}function s(t){return e.expr.filters.visible(t)&&!e(t).parents().andSelf().filter(function(){return e.css(this,"visibility")==="hidden"}).length}var n=0,r=/^ui-id-\d+$/;e.ui=e.ui||{};if(e.ui.version)return;e.extend(e.ui,{version:"1.9.2",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({_focus:e.fn.focus,focus:function(t,n){return typeof t=="number"?this.each(function(){var r=this;setTimeout(function(){e(r).focus(),n&&n.call(r)},t)}):this._focus.apply(this,arguments)},
scrollParent:function(){var t;return e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?t=this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):t=this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(n){if(n!==t)return this.css("zIndex",n);if(this.length){var r=e(this[0]),i,s;while(r.length&&r[0]!==document){i=r.css("position");if(i==="absolute"||i==="relative"||i==="fixed"){s=parseInt(r.css("zIndex"),10);if(!isNaN(s)&&s!==0)return s}r=r.parent()}}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++n)})},removeUniqueId:function(){return this.each(function(){r.test(this.id)&&e(this).removeAtt
r("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(n){return!!e.data(n,t)}}):function(t,n,r){return!!e.data(t,r[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var n=e.attr(t,"tabindex"),r=isNaN(n);return(r||n>=0)&&i(t,!r)}}),e(function(){var t=document.body,n=t.appendChild(n=document.createElement("div"));n.offsetHeight,e.extend(n.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0}),e.support.minHeight=n.offsetHeight===100,e.support.selectstart="onselectstart"in n,t.removeChild(n).style.display="none"}),e("<a>").outerWidth(1).jquery||e.each(["Width","Height"],function(n,r){function u(t,n,r,s){return e.each(i,function(){n-=parseFloat(e.css(t,"padding"+this))||0,r&&(n-=parseFloat(e.css(t,"border"+this+"Width"))||0),s&&(n-=parseFloat(e.css(t,"margin"+this))||0)}),n}var i=r==="Width"?["Left","Right"]:["Top","Bottom"],s=r.toLowerCase(),o={innerWidth:e.fn.innerWidth,inne
rHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+r]=function(n){return n===t?o["inner"+r].call(this):this.each(function(){e(this).css(s,u(this,n)+"px")})},e.fn["outer"+r]=function(t,n){return typeof t!="number"?o["outer"+r].call(this,t):this.each(function(){e(this).css(s,u(this,t,!0,n)+"px")})}}),e("<a>").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(n){return arguments.length?t.call(this,e.camelCase(n)):t.call(this)}}(e.fn.removeData)),function(){var t=/msie ([\w.]+)/.exec(navigator.userAgent.toLowerCase())||[];e.ui.ie=t.length?!0:!1,e.ui.ie6=parseFloat(t[1],10)===6}(),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,n,r){var i,s=e.ui[t].prototype;for(i in r)s.
plugins[i]=s.plugins[i]||[],s.plugins[i].push([n,r[i]])},call:function(e,t,n){var r,i=e.plugins[t];if(!i||!e.element[0].parentNode||e.element[0].parentNode.nodeType===11)return;for(r=0;r<i.length;r++)e.options[i[r][0]]&&i[r][1].apply(e.element,n)}},contains:e.contains,hasScroll:function(t,n){if(e(t).css("overflow")==="hidden")return!1;var r=n&&n==="left"?"scrollLeft":"scrollTop",i=!1;return t[r]>0?!0:(t[r]=1,i=t[r]>0,t[r]=0,i)},isOverAxis:function(e,t,n){return e>t&&e<t+n},isOver:function(t,n,r,i,s,o){return e.ui.isOverAxis(t,r,s)&&e.ui.isOverAxis(n,i,o)}})})(jQuery);(function(e,t){var n=0,r=Array.prototype.slice,i=e.cleanData;e.cleanData=function(t){for(var n=0,r;(r=t[n])!=null;n++)try{e(r).triggerHandler("remove")}catch(s){}i(t)},e.widget=function(t,n,r){var i,s,o,u,a=t.split(".")[0];t=t.split(".")[1],i=a+"-"+t,r||(r=n,n=e.Widget),e.expr[":"][i.toLowerCase()]=function(t){return!!e.data(t,i)},e[a]=e[a]||{},s=e[a][t],o=e[a][t]=function(e,t){if(!this._createWidget)return new
o(e,t);arguments.length&&this._createWidget(e,t)},e.extend(o,s,{version:r.version,_proto:e.extend({},r),_childConstructors:[]}),u=new n,u.options=e.widget.extend({},u.options),e.each(r,function(t,i){e.isFunction(i)&&(r[t]=function(){var e=function(){return n.prototype[t].apply(this,arguments)},r=function(e){return n.prototype[t].apply(this,e)};return function(){var t=this._super,n=this._superApply,s;return this._super=e,this._superApply=r,s=i.apply(this,arguments),this._super=t,this._superApply=n,s}}())}),o.prototype=e.widget.extend(u,{widgetEventPrefix:s?u.widgetEventPrefix:t},r,{constructor:o,namespace:a,widgetName:t,widgetBaseClass:i,widgetFullName:i}),s?(e.each(s._childConstructors,function(t,n){var r=n.prototype;e.widget(r.namespace+"."+r.widgetName,o,n._proto)}),delete s._childConstructors):n._childConstructors.push(o),e.widget.bridge(t,o)},e.widget.extend=function(n){var i=r.call(arguments,1),s=0,o=i.length,u,a;for(;s<o;s++)for(u in i[s])a=i[s][u],i[s].hasOwnProperty(
u)&&a!==t&&(e.isPlainObject(a)?n[u]=e.isPlainObject(n[u])?e.widget.extend({},n[u],a):e.widget.extend({},a):n[u]=a);return n},e.widget.bridge=function(n,i){var s=i.prototype.widgetFullName||n;e.fn[n]=function(o){var u=typeof o=="string",a=r.call(arguments,1),f=this;return o=!u&&a.length?e.widget.extend.apply(null,[o].concat(a)):o,u?this.each(function(){var r,i=e.data(this,s);if(!i)return e.error("cannot call methods on "+n+" prior to initialization; "+"attempted to call method '"+o+"'");if(!e.isFunction(i[o])||o.charAt(0)==="_")return e.error("no such method '"+o+"' for "+n+" widget instance");r=i[o].apply(i,a);if(r!==i&&r!==t)return f=r&&r.jquery?f.pushStack(r.get()):r,!1}):this.each(function(){var t=e.data(this,s);t?t.option(o||{})._init():e.data(this,s,new i(o,this))}),f}},e.Widget=function(){},e.Widget._childConstructors=[],e.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"<div>",options:{disabled:!1,create:null},_createWidget:function(t,r){r=e(
r||this.defaultElement||this)[0],this.element=e(r),this.uuid=n++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=e.widget.extend({},this.options,this._getCreateOptions(),t),this.bindings=e(),this.hoverable=e(),this.focusable=e(),r!==this&&(e.data(r,this.widgetName,this),e.data(r,this.widgetFullName,this),this._on(!0,this.element,{remove:function(e){e.target===r&&this.destroy()}}),this.document=e(r.style?r.ownerDocument:r.document||r),this.window=e(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:e.noop,_getCreateEventData:e.noop,_create:e.noop,_init:e.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(e.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "
+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:e.noop,widget:function(){return this.element},option:function(n,r){var i=n,s,o,u;if(arguments.length===0)return e.widget.extend({},this.options);if(typeof n=="string"){i={},s=n.split("."),n=s.shift();if(s.length){o=i[n]=e.widget.extend({},this.options[n]);for(u=0;u<s.length-1;u++)o[s[u]]=o[s[u]]||{},o=o[s[u]];n=s.pop();if(r===t)return o[n]===t?null:o[n];o[n]=r}else{if(r===t)return this.options[n]===t?null:this.options[n];i[n]=r}}return this._setOptions(i),this},_setOptions:function(e){var t;for(t in e)this._setOption(t,e[t]);return this},_setOption:function(e,t){return this.options[e]=t,e==="disabled"&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!t).attr("aria-disabled",t),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:funct
ion(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(t,n,r){var i,s=this;typeof t!="boolean"&&(r=n,n=t,t=!1),r?(n=i=e(n),this.bindings=this.bindings.add(n)):(r=n,n=this.element,i=this.widget()),e.each(r,function(r,o){function u(){if(!t&&(s.options.disabled===!0||e(this).hasClass("ui-state-disabled")))return;return(typeof o=="string"?s[o]:o).apply(s,arguments)}typeof o!="string"&&(u.guid=o.guid=o.guid||u.guid||e.guid++);var a=r.match(/^(\w+)\s*(.*)$/),f=a[1]+s.eventNamespace,l=a[2];l?i.delegate(l,f,u):n.bind(f,u)})},_off:function(e,t){t=(t||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.unbind(t).undelegate(t)},_delay:function(e,t){function n(){return(typeof e=="string"?r[e]:e).apply(r,arguments)}var r=this;return setTimeout(n,t||0)},_hoverable:function(t){this.hoverable=this.hoverable.add(t),this._on(t,{mouseenter:function(t){e(t.currentTarget).addClass("ui-state-hover")},mouseleave:function(t
){e(t.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(t){this.focusable=this.focusable.add(t),this._on(t,{focusin:function(t){e(t.currentTarget).addClass("ui-state-focus")},focusout:function(t){e(t.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(t,n,r){var i,s,o=this.options[t];r=r||{},n=e.Event(n),n.type=(t===this.widgetEventPrefix?t:this.widgetEventPrefix+t).toLowerCase(),n.target=this.element[0],s=n.originalEvent;if(s)for(i in s)i in n||(n[i]=s[i]);return this.element.trigger(n,r),!(e.isFunction(o)&&o.apply(this.element[0],[n].concat(r))===!1||n.isDefaultPrevented())}},e.each({show:"fadeIn",hide:"fadeOut"},function(t,n){e.Widget.prototype["_"+t]=function(r,i,s){typeof i=="string"&&(i={effect:i});var o,u=i?i===!0||typeof i=="number"?n:i.effect||n:t;i=i||{},typeof i=="number"&&(i={duration:i}),o=!e.isEmptyObject(i),i.complete=s,i.delay&&r.delay(i.delay),o&&e.effects&&(e.effects.effect[u]||e.uiBackCompat!==!1&&e.effects[u])?r[t](i):u!=
=t&&r[u]?r[u](i.duration,i.easing,s):r.queue(function(n){e(this)[t](),s&&s.call(r[0]),n()})}}),e.uiBackCompat!==!1&&(e.Widget.prototype._getCreateOptions=function(){return e.metadata&&e.metadata.get(this.element[0])[this.widgetName]})})(jQuery);(function(e,t){var n=!1;e(document).mouseup(function(e){n=!1}),e.widget("ui.mouse",{version:"1.9.2",options:{cancel:"input,textarea,button,select,option",distance:1,delay:0},_mouseInit:function(){var t=this;this.element.bind("mousedown."+this.widgetName,function(e){return t._mouseDown(e)}).bind("click."+this.widgetName,function(n){if(!0===e.data(n.target,t.widgetName+".preventClickEvent"))return e.removeData(n.target,t.widgetName+".preventClickEvent"),n.stopImmediatePropagation(),!1}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&e(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:func
tion(t){if(n)return;this._mouseStarted&&this._mouseUp(t),this._mouseDownEvent=t;var r=this,i=t.which===1,s=typeof this.options.cancel=="string"&&t.target.nodeName?e(t.target).closest(this.options.cancel).length:!1;if(!i||s||!this._mouseCapture(t))return!0;this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){r.mouseDelayMet=!0},this.options.delay));if(this._mouseDistanceMet(t)&&this._mouseDelayMet(t)){this._mouseStarted=this._mouseStart(t)!==!1;if(!this._mouseStarted)return t.preventDefault(),!0}return!0===e.data(t.target,this.widgetName+".preventClickEvent")&&e.removeData(t.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(e){return r._mouseMove(e)},this._mouseUpDelegate=function(e){return r._mouseUp(e)},e(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),t.preventDefault(),n=!0,!0},_mouseMove:function(t){return!e.ui.ie||doc
ument.documentMode>=9||!!t.button?this._mouseStarted?(this._mouseDrag(t),t.preventDefault()):(this._mouseDistanceMet(t)&&this._mouseDelayMet(t)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,t)!==!1,this._mouseStarted?this._mouseDrag(t):this._mouseUp(t)),!this._mouseStarted):this._mouseUp(t)},_mouseUp:function(t){return e(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,t.target===this._mouseDownEvent.target&&e.data(t.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(t)),!1},_mouseDistanceMet:function(e){return Math.max(Math.abs(this._mouseDownEvent.pageX-e.pageX),Math.abs(this._mouseDownEvent.pageY-e.pageY))>=this.options.distance},_mouseDelayMet:function(e){return this.mouseDelayMet},_mouseStart:function(e){},_mouseDrag:function(e){},_mouseStop:function(e){},_mouseCapture:function(e){return!0}})})(jQuery);(function(e,t){functio
n h(e,t,n){return[parseInt(e[0],10)*(l.test(e[0])?t/100:1),parseInt(e[1],10)*(l.test(e[1])?n/100:1)]}function p(t,n){return parseInt(e.css(t,n),10)||0}e.ui=e.ui||{};var n,r=Math.max,i=Math.abs,s=Math.round,o=/left|center|right/,u=/top|center|bottom/,a=/[\+\-]\d+%?/,f=/^\w+/,l=/%$/,c=e.fn.position;e.position={scrollbarWidth:function(){if(n!==t)return n;var r,i,s=e("<div style='display:block;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>"),o=s.children()[0];return e("body").append(s),r=o.offsetWidth,s.css("overflow","scroll"),i=o.offsetWidth,r===i&&(i=s[0].clientWidth),s.remove(),n=r-i},getScrollInfo:function(t){var n=t.isWindow?"":t.element.css("overflow-x"),r=t.isWindow?"":t.element.css("overflow-y"),i=n==="scroll"||n==="auto"&&t.width<t.element[0].scrollWidth,s=r==="scroll"||r==="auto"&&t.height<t.element[0].scrollHeight;return{width:i?e.position.scrollbarWidth():0,height:s?e.position.scrollbarWidth():0}},getWithinInfo:function(t
){var n=e(t||window),r=e.isWindow(n[0]);return{element:n,isWindow:r,offset:n.offset()||{left:0,top:0},scrollLeft:n.scrollLeft(),scrollTop:n.scrollTop(),width:r?n.width():n.outerWidth(),height:r?n.height():n.outerHeight()}}},e.fn.position=function(t){if(!t||!t.of)return c.apply(this,arguments);t=e.extend({},t);var n,l,d,v,m,g=e(t.of),y=e.position.getWithinInfo(t.within),b=e.position.getScrollInfo(y),w=g[0],E=(t.collision||"flip").split(" "),S={};return w.nodeType===9?(l=g.width(),d=g.height(),v={top:0,left:0}):e.isWindow(w)?(l=g.width(),d=g.height(),v={top:g.scrollTop(),left:g.scrollLeft()}):w.preventDefault?(t.at="left top",l=d=0,v={top:w.pageY,left:w.pageX}):(l=g.outerWidth(),d=g.outerHeight(),v=g.offset()),m=e.extend({},v),e.each(["my","at"],function(){var e=(t[this]||"").split(" "),n,r;e.length===1&&(e=o.test(e[0])?e.concat(["center"]):u.test(e[0])?["center"].concat(e):["center","center"]),e[0]=o.test(e[0])?e[0]:"center",e[1]=u.test(e[1])?e[1]:"center",n=a.exec(e[0]),r=a.
exec(e[1]),S[this]=[n?n[0]:0,r?r[0]:0],t[this]=[f.exec(e[0])[0],f.exec(e[1])[0]]}),E.length===1&&(E[1]=E[0]),t.at[0]==="right"?m.left+=l:t.at[0]==="center"&&(m.left+=l/2),t.at[1]==="bottom"?m.top+=d:t.at[1]==="center"&&(m.top+=d/2),n=h(S.at,l,d),m.left+=n[0],m.top+=n[1],this.each(function(){var o,u,a=e(this),f=a.outerWidth(),c=a.outerHeight(),w=p(this,"marginLeft"),x=p(this,"marginTop"),T=f+w+p(this,"marginRight")+b.width,N=c+x+p(this,"marginBottom")+b.height,C=e.extend({},m),k=h(S.my,a.outerWidth(),a.outerHeight());t.my[0]==="right"?C.left-=f:t.my[0]==="center"&&(C.left-=f/2),t.my[1]==="bottom"?C.top-=c:t.my[1]==="center"&&(C.top-=c/2),C.left+=k[0],C.top+=k[1],e.support.offsetFractions||(C.left=s(C.left),C.top=s(C.top)),o={marginLeft:w,marginTop:x},e.each(["left","top"],function(r,i){e.ui.position[E[r]]&&e.ui.position[E[r]][i](C,{targetWidth:l,targetHeight:d,elemWidth:f,elemHeight:c,collisionPosition:o,collisionWidth:T,collisionHeight:N,offset:[n[0]+k[0],n[1]+k[1]],my:t.my,
at:t.at,within:y,elem:a})}),e.fn.bgiframe&&a.bgiframe(),t.using&&(u=function(e){var n=v.left-C.left,s=n+l-f,o=v.top-C.top,u=o+d-c,h={target:{element:g,left:v.left,top:v.top,width:l,height:d},element:{element:a,left:C.left,top:C.top,width:f,height:c},horizontal:s<0?"left":n>0?"right":"center",vertical:u<0?"top":o>0?"bottom":"middle"};l<f&&i(n+s)<l&&(h.horizontal="center"),d<c&&i(o+u)<d&&(h.vertical="middle"),r(i(n),i(s))>r(i(o),i(u))?h.important="horizontal":h.important="vertical",t.using.call(this,e,h)}),a.offset(e.extend(C,{using:u}))})},e.ui.position={fit:{left:function(e,t){var n=t.within,i=n.isWindow?n.scrollLeft:n.offset.left,s=n.width,o=e.left-t.collisionPosition.marginLeft,u=i-o,a=o+t.collisionWidth-s-i,f;t.collisionWidth>s?u>0&&a<=0?(f=e.left+u+t.collisionWidth-s-i,e.left+=u-f):a>0&&u<=0?e.left=i:u>a?e.left=i+s-t.collisionWidth:e.left=i:u>0?e.left+=u:a>0?e.left-=a:e.left=r(e.left-o,e.left)},top:function(e,t){var n=t.within,i=n.isWindow?n.scrollTop:n.offset.top,s=t.wi
thin.height,o=e.top-t.collisionPosition.marginTop,u=i-o,a=o+t.collisionHeight-s-i,f;t.collisionHeight>s?u>0&&a<=0?(f=e.top+u+t.collisionHeight-s-i,e.top+=u-f):a>0&&u<=0?e.top=i:u>a?e.top=i+s-t.collisionHeight:e.top=i:u>0?e.top+=u:a>0?e.top-=a:e.top=r(e.top-o,e.top)}},flip:{left:function(e,t){var n=t.within,r=n.offset.left+n.scrollLeft,s=n.width,o=n.isWindow?n.scrollLeft:n.offset.left,u=e.left-t.collisionPosition.marginLeft,a=u-o,f=u+t.collisionWidth-s-o,l=t.my[0]==="left"?-t.elemWidth:t.my[0]==="right"?t.elemWidth:0,c=t.at[0]==="left"?t.targetWidth:t.at[0]==="right"?-t.targetWidth:0,h=-2*t.offset[0],p,d;if(a<0){p=e.left+l+c+h+t.collisionWidth-s-r;if(p<0||p<i(a))e.left+=l+c+h}else if(f>0){d=e.left-t.collisionPosition.marginLeft+l+c+h-o;if(d>0||i(d)<f)e.left+=l+c+h}},top:function(e,t){var n=t.within,r=n.offset.top+n.scrollTop,s=n.height,o=n.isWindow?n.scrollTop:n.offset.top,u=e.top-t.collisionPosition.marginTop,a=u-o,f=u+t.collisionHeight-s-o,l=t.my[1]==="top",c=l?-t.elemHeigh
t:t.my[1]==="bottom"?t.elemHeight:0,h=t.at[1]==="top"?t.targetHeight:t.at[1]==="bottom"?-t.targetHeight:0,p=-2*t.offset[1],d,v;a<0?(v=e.top+c+h+p+t.collisionHeight-s-r,e.top+c+h+p>a&&(v<0||v<i(a))&&(e.top+=c+h+p)):f>0&&(d=e.top-t.collisionPosition.marginTop+c+h+p-o,e.top+c+h+p>f&&(d>0||i(d)<f)&&(e.top+=c+h+p))}},flipfit:{left:function(){e.ui.position.flip.left.apply(this,arguments),e.ui.position.fit.left.apply(this,arguments)},top:function(){e.ui.position.flip.top.apply(this,arguments),e.ui.position.fit.top.apply(this,arguments)}}},function(){var t,n,r,i,s,o=document.getElementsByTagName("body")[0],u=document.createElement("div");t=document.createElement(o?"div":"body"),r={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},o&&e.extend(r,{position:"absolute",left:"-1000px",top:"-1000px"});for(s in r)t.style[s]=r[s];t.appendChild(u),n=o||document.documentElement,n.insertBefore(t,n.firstChild),u.style.cssText="position: absolute; left: 10.7432222px;",i=e(
u).offset().left,e.support.offsetFractions=i>10&&i<11,t.innerHTML="",n.removeChild(t)}(),e.uiBackCompat!==!1&&function(e){var n=e.fn.position;e.fn.position=function(r){if(!r||!r.offset)return n.call(this,r);var i=r.offset.split(" "),s=r.at.split(" ");return i.length===1&&(i[1]=i[0]),/^\d/.test(i[0])&&(i[0]="+"+i[0]),/^\d/.test(i[1])&&(i[1]="+"+i[1]),s.length===1&&(/left|center|right/.test(s[0])?s[1]="center":(s[1]=s[0],s[0]="center")),n.call(this,e.extend(r,{at:s[0]+i[0]+" "+s[1]+i[1],offset:t}))}}(jQuery)})(jQuery);(function(e,t){e.widget("ui.draggable",e.ui.mouse,{version:"1.9.2",widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1},_create:function(){this.options.
helper=="original"&&!/^(?:r|a|f)/.test(this.element.css("position"))&&(this.element[0].style.position="relative"),this.options.addClasses&&this.element.addClass("ui-draggable"),this.options.disabled&&this.element.addClass("ui-draggable-disabled"),this._mouseInit()},_destroy:function(){this.element.removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"),this._mouseDestroy()},_mouseCapture:function(t){var n=this.options;return this.helper||n.disabled||e(t.target).is(".ui-resizable-handle")?!1:(this.handle=this._getHandle(t),this.handle?(e(n.iframeFix===!0?"iframe":n.iframeFix).each(function(){e('<div class="ui-draggable-iframeFix" style="background: #fff;"></div>').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1e3}).css(e(this).offset()).appendTo("body")}),!0):!1)},_mouseStart:function(t){var n=this.options;return this.helper=this._createHelper(t),this.helper.addClass("ui-draggable-dragging"),this._cac
heHelperProportions(),e.ui.ddmanager&&(e.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(),this.offset=this.positionAbs=this.element.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},e.extend(this.offset,{click:{left:t.pageX-this.offset.left,top:t.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.originalPosition=this.position=this._generatePosition(t),this.originalPageX=t.pageX,this.originalPageY=t.pageY,n.cursorAt&&this._adjustOffsetFromHelper(n.cursorAt),n.containment&&this._setContainment(),this._trigger("start",t)===!1?(this._clear(),!1):(this._cacheHelperProportions(),e.ui.ddmanager&&!n.dropBehaviour&&e.ui.ddmanager.prepareOffsets(this,t),this._mouseDrag(t,!0),e.ui.ddmanager&&e.ui.ddmanager.dragStart(this,t),!0)},_mouseDrag:function(t,n){this.position=this._generatePosition(t),this.positi
onAbs=this._convertPositionTo("absolute");if(!n){var r=this._uiHash();if(this._trigger("drag",t,r)===!1)return this._mouseUp({}),!1;this.position=r.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";return e.ui.ddmanager&&e.ui.ddmanager.drag(this,t),!1},_mouseStop:function(t){var n=!1;e.ui.ddmanager&&!this.options.dropBehaviour&&(n=e.ui.ddmanager.drop(this,t)),this.dropped&&(n=this.dropped,this.dropped=!1);var r=this.element[0],i=!1;while(r&&(r=r.parentNode))r==document&&(i=!0);if(!i&&this.options.helper==="original")return!1;if(this.options.revert=="invalid"&&!n||this.options.revert=="valid"&&n||this.options.revert===!0||e.isFunction(this.options.revert)&&this.options.revert.call(this.element,n)){var s=this;e(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){s._trigger("stop",t)!==!1&
&s._clear()})}else this._trigger("stop",t)!==!1&&this._clear();return!1},_mouseUp:function(t){return e("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)}),e.ui.ddmanager&&e.ui.ddmanager.dragStop(this,t),e.ui.mouse.prototype._mouseUp.call(this,t)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear(),this},_getHandle:function(t){var n=!this.options.handle||!e(this.options.handle,this.element).length?!0:!1;return e(this.options.handle,this.element).find("*").andSelf().each(function(){this==t.target&&(n=!0)}),n},_createHelper:function(t){var n=this.options,r=e.isFunction(n.helper)?e(n.helper.apply(this.element[0],[t])):n.helper=="clone"?this.element.clone().removeAttr("id"):this.element;return r.parents("body").length||r.appendTo(n.appendTo=="parent"?this.element[0].parentNode:n.appendTo),r[0]!=this.element[0]&&!/(fixed|absolute)/.test(r.css("position"))&&r.css("position","absolute"),r},_adjustOffsetFromH
elper:function(t){typeof t=="string"&&(t=t.split(" ")),e.isArray(t)&&(t={left:+t[0],top:+t[1]||0}),"left"in t&&(this.offset.click.left=t.left+this.margins.left),"right"in t&&(this.offset.click.left=this.helperProportions.width-t.right+this.margins.left),"top"in t&&(this.offset.click.top=t.top+this.margins.top),"bottom"in t&&(this.offset.click.top=this.helperProportions.height-t.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var t=this.offsetParent.offset();this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&e.contains(this.scrollParent[0],this.offsetParent[0])&&(t.left+=this.scrollParent.scrollLeft(),t.top+=this.scrollParent.scrollTop());if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&e.ui.ie)t={top:0,left:0};return{top:t.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:t.left+(parseInt(this.offsetParent.css("borderLeftWidt
h"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var e=this.element.position();return{top:e.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:e.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var t=this.options;t.containment=="parent"&&(t.containment=this.helper[0].parentNode);if(t.containment=="document"||t.containment=="window")this.containment=[t.containment=="document"?0:e(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,t.containment=="docume
nt"?0:e(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,(t.containment=="document"?0:e(window).scrollLeft())+e(t.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(t.containment=="document"?0:e(window).scrollTop())+(e(t.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(t.containment)&&t.containment.constructor!=Array){var n=e(t.containment),r=n[0];if(!r)return;var i=n.offset(),s=e(r).css("overflow")!="hidden";this.containment=[(parseInt(e(r).css("borderLeftWidth"),10)||0)+(parseInt(e(r).css("paddingLeft"),10)||0),(parseInt(e(r).css("borderTopWidth"),10)||0)+(parseInt(e(r).css("paddingTop"),10)||0),(s?Math.max(r.scrollWidth,r.offsetWidth):r.offsetWidth)-(parseInt(e(r).css("borderLeftWidth"),10)||0)-(parseInt(e(r).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-
this.margins.right,(s?Math.max(r.scrollHeight,r.offsetHeight):r.offsetHeight)-(parseInt(e(r).css("borderTopWidth"),10)||0)-(parseInt(e(r).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relative_container=n}else t.containment.constructor==Array&&(this.containment=t.containment)},_convertPositionTo:function(t,n){n||(n=this.position);var r=t=="absolute"?1:-1,i=this.options,s=this.cssPosition!="absolute"||this.scrollParent[0]!=document&&!!e.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,o=/(html|body)/i.test(s[0].tagName);return{top:n.top+this.offset.relative.top*r+this.offset.parent.top*r-(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():o?0:s.scrollTop())*r,left:n.left+this.offset.relative.left*r+this.offset.parent.left*r-(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():o?0:s.scrollLeft())*r}},_generatePosition:function(t){var n=this.options,r=this.cssPosition!="ab
solute"||this.scrollParent[0]!=document&&!!e.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,i=/(html|body)/i.test(r[0].tagName),s=t.pageX,o=t.pageY;if(this.originalPosition){var u;if(this.containment){if(this.relative_container){var a=this.relative_container.offset();u=[this.containment[0]+a.left,this.containment[1]+a.top,this.containment[2]+a.left,this.containment[3]+a.top]}else u=this.containment;t.pageX-this.offset.click.left<u[0]&&(s=u[0]+this.offset.click.left),t.pageY-this.offset.click.top<u[1]&&(o=u[1]+this.offset.click.top),t.pageX-this.offset.click.left>u[2]&&(s=u[2]+this.offset.click.left),t.pageY-this.offset.click.top>u[3]&&(o=u[3]+this.offset.click.top)}if(n.grid){var f=n.grid[1]?this.originalPageY+Math.round((o-this.originalPageY)/n.grid[1])*n.grid[1]:this.originalPageY;o=u?f-this.offset.click.top<u[1]||f-this.offset.click.top>u[3]?f-this.offset.click.top<u[1]?f+n.grid[1]:f-n.grid[1]:f:f;var l=n.grid[0]?this.originalPageX
+Math.round((s-this.originalPageX)/n.grid[0])*n.grid[0]:this.originalPageX;s=u?l-this.offset.click.left<u[0]||l-this.offset.click.left>u[2]?l-this.offset.click.left<u[0]?l+n.grid[0]:l-n.grid[0]:l:l}}return{top:o-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():i?0:r.scrollTop()),left:s-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():i?0:r.scrollLeft())}},_clear:function(){this.helper.removeClass("ui-draggable-dragging"),this.helper[0]!=this.element[0]&&!this.cancelHelperRemoval&&this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1},_trigger:function(t,n,r){return r=r||this._uiHash(),e.ui.plugin.call(this,t,[n,r]),t=="drag"&&(this.positionAbs=this._convertPositionTo("absolute")),e.Widget.prototype._trigger.call(this,t,n,r)},plugins:{},_uiHash:function(e){return{helper:this.helper,position:this.position,
originalPosition:this.originalPosition,offset:this.positionAbs}}}),e.ui.plugin.add("draggable","connectToSortable",{start:function(t,n){var r=e(this).data("draggable"),i=r.options,s=e.extend({},n,{item:r.element});r.sortables=[],e(i.connectToSortable).each(function(){var n=e.data(this,"sortable");n&&!n.options.disabled&&(r.sortables.push({instance:n,shouldRevert:n.options.revert}),n.refreshPositions(),n._trigger("activate",t,s))})},stop:function(t,n){var r=e(this).data("draggable"),i=e.extend({},n,{item:r.element});e.each(r.sortables,function(){this.instance.isOver?(this.instance.isOver=0,r.cancelHelperRemoval=!0,this.instance.cancelHelperRemoval=!1,this.shouldRevert&&(this.instance.options.revert=!0),this.instance._mouseStop(t),this.instance.options.helper=this.instance.options._helper,r.options.helper=="original"&&this.instance.currentItem.css({top:"auto",left:"auto"})):(this.instance.cancelHelperRemoval=!1,this.instance._trigger("deactivate",t,i))})},drag:function(t,n){va
r r=e(this).data("draggable"),i=this,s=function(t){var n=this.offset.click.top,r=this.offset.click.left,i=this.positionAbs.top,s=this.positionAbs.left,o=t.height,u=t.width,a=t.top,f=t.left;return e.ui.isOver(i+n,s+r,a,f,o,u)};e.each(r.sortables,function(s){var o=!1,u=this;this.instance.positionAbs=r.positionAbs,this.instance.helperProportions=r.helperProportions,this.instance.offset.click=r.offset.click,this.instance._intersectsWith(this.instance.containerCache)&&(o=!0,e.each(r.sortables,function(){return this.instance.positionAbs=r.positionAbs,this.instance.helperProportions=r.helperProportions,this.instance.offset.click=r.offset.click,this!=u&&this.instance._intersectsWith(this.instance.containerCache)&&e.ui.contains(u.instance.element[0],this.instance.element[0])&&(o=!1),o})),o?(this.instance.isOver||(this.instance.isOver=1,this.instance.currentItem=e(i).clone().removeAttr("id").appendTo(this.instance.element).data("sortable-item",!0),this.instance.options._helper=this.in
stance.options.helper,this.instance.options.helper=function(){return n.helper[0]},t.target=this.instance.currentItem[0],this.instance._mouseCapture(t,!0),this.instance._mouseStart(t,!0,!0),this.instance.offset.click.top=r.offset.click.top,this.instance.offset.click.left=r.offset.click.left,this.instance.offset.parent.left-=r.offset.parent.left-this.instance.offset.parent.left,this.instance.offset.parent.top-=r.offset.parent.top-this.instance.offset.parent.top,r._trigger("toSortable",t),r.dropped=this.instance.element,r.currentItem=r.element,this.instance.fromOutside=r),this.instance.currentItem&&this.instance._mouseDrag(t)):this.instance.isOver&&(this.instance.isOver=0,this.instance.cancelHelperRemoval=!0,this.instance.options.revert=!1,this.instance._trigger("out",t,this.instance._uiHash(this.instance)),this.instance._mouseStop(t,!0),this.instance.options.helper=this.instance.options._helper,this.instance.currentItem.remove(),this.instance.placeholder&&this.instance.placeho
lder.remove(),r._trigger("fromSortable",t),r.dropped=!1)})}}),e.ui.plugin.add("draggable","cursor",{start:function(t,n){var r=e("body"),i=e(this).data("draggable").options;r.css("cursor")&&(i._cursor=r.css("cursor")),r.css("cursor",i.cursor)},stop:function(t,n){var r=e(this).data("draggable").options;r._cursor&&e("body").css("cursor",r._cursor)}}),e.ui.plugin.add("draggable","opacity",{start:function(t,n){var r=e(n.helper),i=e(this).data("draggable").options;r.css("opacity")&&(i._opacity=r.css("opacity")),r.css("opacity",i.opacity)},stop:function(t,n){var r=e(this).data("draggable").options;r._opacity&&e(n.helper).css("opacity",r._opacity)}}),e.ui.plugin.add("draggable","scroll",{start:function(t,n){var r=e(this).data("draggable");r.scrollParent[0]!=document&&r.scrollParent[0].tagName!="HTML"&&(r.overflowOffset=r.scrollParent.offset())},drag:function(t,n){var r=e(this).data("draggable"),i=r.options,s=!1;if(r.scrollParent[0]!=document&&r.scrollParent[0].tagName!="HTML"){if(!i
.axis||i.axis!="x")r.overflowOffset.top+r.scrollParent[0].offsetHeight-t.pageY<i.scrollSensitivity?r.scrollParent[0].scrollTop=s=r.scrollParent[0].scrollTop+i.scrollSpeed:t.pageY-r.overflowOffset.top<i.scrollSensitivity&&(r.scrollParent[0].scrollTop=s=r.scrollParent[0].scrollTop-i.scrollSpeed);if(!i.axis||i.axis!="y")r.overflowOffset.left+r.scrollParent[0].offsetWidth-t.pageX<i.scrollSensitivity?r.scrollParent[0].scrollLeft=s=r.scrollParent[0].scrollLeft+i.scrollSpeed:t.pageX-r.overflowOffset.left<i.scrollSensitivity&&(r.scrollParent[0].scrollLeft=s=r.scrollParent[0].scrollLeft-i.scrollSpeed)}else{if(!i.axis||i.axis!="x")t.pageY-e(document).scrollTop()<i.scrollSensitivity?s=e(document).scrollTop(e(document).scrollTop()-i.scrollSpeed):e(window).height()-(t.pageY-e(document).scrollTop())<i.scrollSensitivity&&(s=e(document).scrollTop(e(document).scrollTop()+i.scrollSpeed));if(!i.axis||i.axis!="y")t.pageX-e(document).scrollLeft()<i.scrollSensitivity?s=e(document).scrollLeft(e(do
cument).scrollLeft()-i.scrollSpeed):e(window).width()-(t.pageX-e(document).scrollLeft())<i.scrollSensitivity&&(s=e(document).scrollLeft(e(document).scrollLeft()+i.scrollSpeed))}s!==!1&&e.ui.ddmanager&&!i.dropBehaviour&&e.ui.ddmanager.prepareOffsets(r,t)}}),e.ui.plugin.add("draggable","snap",{start:function(t,n){var r=e(this).data("draggable"),i=r.options;r.snapElements=[],e(i.snap.constructor!=String?i.snap.items||":data(draggable)":i.snap).each(function(){var t=e(this),n=t.offset();this!=r.element[0]&&r.snapElements.push({item:this,width:t.outerWidth(),height:t.outerHeight(),top:n.top,left:n.left})})},drag:function(t,n){var r=e(this).data("draggable"),i=r.options,s=i.snapTolerance,o=n.offset.left,u=o+r.helperProportions.width,a=n.offset.top,f=a+r.helperProportions.height;for(var l=r.snapElements.length-1;l>=0;l--){var c=r.snapElements[l].left,h=c+r.snapElements[l].width,p=r.snapElements[l].top,d=p+r.snapElements[l].height;if(!(c-s<o&&o<h+s&&p-s<a&&a<d+s||c-s<o&&o<h+s&&p-s<f
&&f<d+s||c-s<u&&u<h+s&&p-s<a&&a<d+s||c-s<u&&u<h+s&&p-s<f&&f<d+s)){r.snapElements[l].snapping&&r.options.snap.release&&r.options.snap.release.call(r.element,t,e.extend(r._uiHash(),{snapItem:r.snapElements[l].item})),r.snapElements[l].snapping=!1;continue}if(i.snapMode!="inner"){var v=Math.abs(p-f)<=s,m=Math.abs(d-a)<=s,g=Math.abs(c-u)<=s,y=Math.abs(h-o)<=s;v&&(n.position.top=r._convertPositionTo("relative",{top:p-r.helperProportions.height,left:0}).top-r.margins.top),m&&(n.position.top=r._convertPositionTo("relative",{top:d,left:0}).top-r.margins.top),g&&(n.position.left=r._convertPositionTo("relative",{top:0,left:c-r.helperProportions.width}).left-r.margins.left),y&&(n.position.left=r._convertPositionTo("relative",{top:0,left:h}).left-r.margins.left)}var b=v||m||g||y;if(i.snapMode!="outer"){var v=Math.abs(p-a)<=s,m=Math.abs(d-f)<=s,g=Math.abs(c-o)<=s,y=Math.abs(h-u)<=s;v&&(n.position.top=r._convertPositionTo("relative",{top:p,left:0}).top-r.margins.top),m&&(n.position.top=r.
_convertPositionTo("relative",{top:d-r.helperProportions.height,left:0}).top-r.margins.top),g&&(n.position.left=r._convertPositionTo("relative",{top:0,left:c}).left-r.margins.left),y&&(n.position.left=r._convertPositionTo("relative",{top:0,left:h-r.helperProportions.width}).left-r.margins.left)}!r.snapElements[l].snapping&&(v||m||g||y||b)&&r.options.snap.snap&&r.options.snap.snap.call(r.element,t,e.extend(r._uiHash(),{snapItem:r.snapElements[l].item})),r.snapElements[l].snapping=v||m||g||y||b}}}),e.ui.plugin.add("draggable","stack",{start:function(t,n){var r=e(this).data("draggable").options,i=e.makeArray(e(r.stack)).sort(function(t,n){return(parseInt(e(t).css("zIndex"),10)||0)-(parseInt(e(n).css("zIndex"),10)||0)});if(!i.length)return;var s=parseInt(i[0].style.zIndex)||0;e(i).each(function(e){this.style.zIndex=s+e}),this[0].style.zIndex=s+i.length}}),e.ui.plugin.add("draggable","zIndex",{start:function(t,n){var r=e(n.helper),i=e(this).data("draggable").options;r.css("zIndex
")&&(i._zIndex=r.css("zIndex")),r.css("zIndex",i.zIndex)},stop:function(t,n){var r=e(this).data("draggable").options;r._zIndex&&e(n.helper).css("zIndex",r._zIndex)}})})(jQuery);(function(e,t){e.widget("ui.resizable",e.ui.mouse,{version:"1.9.2",widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:1e3},_create:function(){var t=this,n=this.options;this.element.addClass("ui-resizable"),e.extend(this,{_aspectRatio:!!n.aspectRatio,aspectRatio:n.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:n.helper||n.ghost||n.animate?n.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)&&(this.element.wrap(e('<div class="ui-wrapper" style="overflow: hidden;"></div>').css({position:this.element.css("positio
n"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("resizable",this.element.data("resizable")),this.elementIsWrapper=!0,this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")}),this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0}),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css({margin:this.originalElement.css("margin")}),this._proportionallyResize()),this.handles=n.handles||(e(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s"
,w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se");if(this.handles.constructor==String){this.handles=="all"&&(this.handles="n,e,s,w,se,sw,ne,nw");var r=this.handles.split(",");this.handles={};for(var i=0;i<r.length;i++){var s=e.trim(r[i]),o="ui-resizable-"+s,u=e('<div class="ui-resizable-handle '+o+'"></div>');u.css({zIndex:n.zIndex}),"se"==s&&u.addClass("ui-icon ui-icon-gripsmall-diagonal-se"),this.handles[s]=".ui-resizable-"+s,this.element.append(u)}}this._renderAxis=function(t){t=t||this.element;for(var n in this.handles){this.handles[n].constructor==String&&(this.handles[n]=e(this.handles[n],this.element).show());if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var r=e(this.handles[n],this.element),i=0;i=/sw|ne|nw|se|n|s/.test(n)?r.outerHeight():r.outerWidth();var s=["padding",/ne|nw|n/.test(n)?"Top":/se|sw|s/.test(n)?"Bottom":/^e$/.test(n)?"Right":"Left
"].join("");t.css(s,i),this._proportionallyResize()}if(!e(this.handles[n]).length)continue}},this._renderAxis(this.element),this._handles=e(".ui-resizable-handle",this.element).disableSelection(),this._handles.mouseover(function(){if(!t.resizing){if(this.className)var e=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);t.axis=e&&e[1]?e[1]:"se"}}),n.autoHide&&(this._handles.hide(),e(this.element).addClass("ui-resizable-autohide").mouseenter(function(){if(n.disabled)return;e(this).removeClass("ui-resizable-autohide"),t._handles.show()}).mouseleave(function(){if(n.disabled)return;t.resizing||(e(this).addClass("ui-resizable-autohide"),t._handles.hide())})),this._mouseInit()},_destroy:function(){this._mouseDestroy();var t=function(t){e(t).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").removeData("ui-resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};if(this.elementIsWrapper){t(this.element);var n=this
.element;this.originalElement.css({position:n.css("position"),width:n.outerWidth(),height:n.outerHeight(),top:n.css("top"),left:n.css("left")}).insertAfter(n),n.remove()}return this.originalElement.css("resize",this.originalResizeStyle),t(this.originalElement),this},_mouseCapture:function(t){var n=!1;for(var r in this.handles)e(this.handles[r])[0]==t.target&&(n=!0);return!this.options.disabled&&n},_mouseStart:function(t){var r=this.options,i=this.element.position(),s=this.element;this.resizing=!0,this.documentScroll={top:e(document).scrollTop(),left:e(document).scrollLeft()},(s.is(".ui-draggable")||/absolute/.test(s.css("position")))&&s.css({position:"absolute",top:i.top,left:i.left}),this._renderProxy();var o=n(this.helper.css("left")),u=n(this.helper.css("top"));r.containment&&(o+=e(r.containment).scrollLeft()||0,u+=e(r.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:o,top:u},this.size=this._helper?{width:s.outerWidth(),height:s.outerHeigh
t()}:{width:s.width(),height:s.height()},this.originalSize=this._helper?{width:s.outerWidth(),height:s.outerHeight()}:{width:s.width(),height:s.height()},this.originalPosition={left:o,top:u},this.sizeDiff={width:s.outerWidth()-s.width(),height:s.outerHeight()-s.height()},this.originalMousePosition={left:t.pageX,top:t.pageY},this.aspectRatio=typeof r.aspectRatio=="number"?r.aspectRatio:this.originalSize.width/this.originalSize.height||1;var a=e(".ui-resizable-"+this.axis).css("cursor");return e("body").css("cursor",a=="auto"?this.axis+"-resize":a),s.addClass("ui-resizable-resizing"),this._propagate("start",t),!0},_mouseDrag:function(e){var t=this.helper,n=this.options,r={},i=this,s=this.originalMousePosition,o=this.axis,u=e.pageX-s.left||0,a=e.pageY-s.top||0,f=this._change[o];if(!f)return!1;var l=f.apply(this,[e,u,a]);this._updateVirtualBoundaries(e.shiftKey);if(this._aspectRatio||e.shiftKey)l=this._updateRatio(l,e);return l=this._respectSize(l,e),this._propagate("resize",e),
t.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"}),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),this._updateCache(l),this._trigger("resize",e,this.ui()),!1},_mouseStop:function(t){this.resizing=!1;var n=this.options,r=this;if(this._helper){var i=this._proportionallyResizeElements,s=i.length&&/textarea/i.test(i[0].nodeName),o=s&&e.ui.hasScroll(i[0],"left")?0:r.sizeDiff.height,u=s?0:r.sizeDiff.width,a={width:r.helper.width()-u,height:r.helper.height()-o},f=parseInt(r.element.css("left"),10)+(r.position.left-r.originalPosition.left)||null,l=parseInt(r.element.css("top"),10)+(r.position.top-r.originalPosition.top)||null;n.animate||this.element.css(e.extend(a,{top:l,left:f})),r.helper.height(r.size.height),r.helper.width(r.size.width),this._helper&&!n.animate&&this._proportionallyResize()}return e("body").css("cursor","auto"),this.element.removeClass("ui-resizable-resiz
ing"),this._propagate("stop",t),this._helper&&this.helper.remove(),!1},_updateVirtualBoundaries:function(e){var t=this.options,n,i,s,o,u;u={minWidth:r(t.minWidth)?t.minWidth:0,maxWidth:r(t.maxWidth)?t.maxWidth:Infinity,minHeight:r(t.minHeight)?t.minHeight:0,maxHeight:r(t.maxHeight)?t.maxHeight:Infinity};if(this._aspectRatio||e)n=u.minHeight*this.aspectRatio,s=u.minWidth/this.aspectRatio,i=u.maxHeight*this.aspectRatio,o=u.maxWidth/this.aspectRatio,n>u.minWidth&&(u.minWidth=n),s>u.minHeight&&(u.minHeight=s),i<u.maxWidth&&(u.maxWidth=i),o<u.maxHeight&&(u.maxHeight=o);this._vBoundaries=u},_updateCache:function(e){var t=this.options;this.offset=this.helper.offset(),r(e.left)&&(this.position.left=e.left),r(e.top)&&(this.position.top=e.top),r(e.height)&&(this.size.height=e.height),r(e.width)&&(this.size.width=e.width)},_updateRatio:function(e,t){var n=this.options,i=this.position,s=this.size,o=this.axis;return r(e.height)?e.width=e.height*this.aspectRatio:r(e.width)&&(e.height=e.wi
dth/this.aspectRatio),o=="sw"&&(e.left=i.left+(s.width-e.width),e.top=null),o=="nw"&&(e.top=i.top+(s.height-e.height),e.left=i.left+(s.width-e.width)),e},_respectSize:function(e,t){var n=this.helper,i=this._vBoundaries,s=this._aspectRatio||t.shiftKey,o=this.axis,u=r(e.width)&&i.maxWidth&&i.maxWidth<e.width,a=r(e.height)&&i.maxHeight&&i.maxHeight<e.height,f=r(e.width)&&i.minWidth&&i.minWidth>e.width,l=r(e.height)&&i.minHeight&&i.minHeight>e.height;f&&(e.width=i.minWidth),l&&(e.height=i.minHeight),u&&(e.width=i.maxWidth),a&&(e.height=i.maxHeight);var c=this.originalPosition.left+this.originalSize.width,h=this.position.top+this.size.height,p=/sw|nw|w/.test(o),d=/nw|ne|n/.test(o);f&&p&&(e.left=c-i.minWidth),u&&p&&(e.left=c-i.maxWidth),l&&d&&(e.top=h-i.minHeight),a&&d&&(e.top=h-i.maxHeight);var v=!e.width&&!e.height;return v&&!e.left&&e.top?e.top=null:v&&!e.top&&e.left&&(e.left=null),e},_proportionallyResize:function(){var t=this.options;if(!this._proportionallyResizeElements.len
gth)return;var n=this.helper||this.element;for(var r=0;r<this._proportionallyResizeElements.length;r++){var i=this._proportionallyResizeElements[r];if(!this.borderDif){var s=[i.css("borderTopWidth"),i.css("borderRightWidth"),i.css("borderBottomWidth"),i.css("borderLeftWidth")],o=[i.css("paddingTop"),i.css("paddingRight"),i.css("paddingBottom"),i.css("paddingLeft")];this.borderDif=e.map(s,function(e,t){var n=parseInt(e,10)||0,r=parseInt(o[t],10)||0;return n+r})}i.css({height:n.height()-this.borderDif[0]-this.borderDif[2]||0,width:n.width()-this.borderDif[1]-this.borderDif[3]||0})}},_renderProxy:function(){var t=this.element,n=this.options;this.elementOffset=t.offset();if(this._helper){this.helper=this.helper||e('<div style="overflow:hidden;"></div>');var r=e.ui.ie6?1:0,i=e.ui.ie6?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+i,height:this.element.outerHeight()+i,position:"absolute",left:this.elementOffset.left-r+"px",top:this.elementOffset.top-r
+"px",zIndex:++n.zIndex}),this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(e,t,n){return{width:this.originalSize.width+t}},w:function(e,t,n){var r=this.options,i=this.originalSize,s=this.originalPosition;return{left:s.left+t,width:i.width-t}},n:function(e,t,n){var r=this.options,i=this.originalSize,s=this.originalPosition;return{top:s.top+n,height:i.height-n}},s:function(e,t,n){return{height:this.originalSize.height+n}},se:function(t,n,r){return e.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[t,n,r]))},sw:function(t,n,r){return e.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[t,n,r]))},ne:function(t,n,r){return e.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[t,n,r]))},nw:function(t,n,r){return e.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[t,n,r]))}},_propagate:function(t,n){e.ui.plugin.call(this,t,[n,this.ui()]),t!="resize"&&t
his._trigger(t,n,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),e.ui.plugin.add("resizable","alsoResize",{start:function(t,n){var r=e(this).data("resizable"),i=r.options,s=function(t){e(t).each(function(){var t=e(this);t.data("resizable-alsoresize",{width:parseInt(t.width(),10),height:parseInt(t.height(),10),left:parseInt(t.css("left"),10),top:parseInt(t.css("top"),10)})})};typeof i.alsoResize=="object"&&!i.alsoResize.parentNode?i.alsoResize.length?(i.alsoResize=i.alsoResize[0],s(i.alsoResize)):e.each(i.alsoResize,function(e){s(e)}):s(i.alsoResize)},resize:function(t,n){var r=e(this).data("resizable"),i=r.options,s=r.originalSize,o=r.originalPosition,u={height:r.size.height-s.height||0,width:r.size.width-s.width||0,top:r.position.top-o.top||0,left:r.position.left-o.left||0},a=function(t,r){e(
t).each(function(){var t=e(this),i=e(this).data("resizable-alsoresize"),s={},o=r&&r.length?r:t.parents(n.originalElement[0]).length?["width","height"]:["width","height","top","left"];e.each(o,function(e,t){var n=(i[t]||0)+(u[t]||0);n&&n>=0&&(s[t]=n||null)}),t.css(s)})};typeof i.alsoResize=="object"&&!i.alsoResize.nodeType?e.each(i.alsoResize,function(e,t){a(e,t)}):a(i.alsoResize)},stop:function(t,n){e(this).removeData("resizable-alsoresize")}}),e.ui.plugin.add("resizable","animate",{stop:function(t,n){var r=e(this).data("resizable"),i=r.options,s=r._proportionallyResizeElements,o=s.length&&/textarea/i.test(s[0].nodeName),u=o&&e.ui.hasScroll(s[0],"left")?0:r.sizeDiff.height,a=o?0:r.sizeDiff.width,f={width:r.size.width-a,height:r.size.height-u},l=parseInt(r.element.css("left"),10)+(r.position.left-r.originalPosition.left)||null,c=parseInt(r.element.css("top"),10)+(r.position.top-r.originalPosition.top)||null;r.element.animate(e.extend(f,c&&l?{top:c,left:l}:{}),{duration:i.anim
ateDuration,easing:i.animateEasing,step:function(){var n={width:parseInt(r.element.css("width"),10),height:parseInt(r.element.css("height"),10),top:parseInt(r.element.css("top"),10),left:parseInt(r.element.css("left"),10)};s&&s.length&&e(s[0]).css({width:n.width,height:n.height}),r._updateCache(n),r._propagate("resize",t)}})}}),e.ui.plugin.add("resizable","containment",{start:function(t,r){var i=e(this).data("resizable"),s=i.options,o=i.element,u=s.containment,a=u instanceof e?u.get(0):/parent/.test(u)?o.parent().get(0):u;if(!a)return;i.containerElement=e(a);if(/document/.test(u)||u==document)i.containerOffset={left:0,top:0},i.containerPosition={left:0,top:0},i.parentData={element:e(document),left:0,top:0,width:e(document).width(),height:e(document).height()||document.body.parentNode.scrollHeight};else{var f=e(a),l=[];e(["Top","Right","Left","Bottom"]).each(function(e,t){l[e]=n(f.css("padding"+t))}),i.containerOffset=f.offset(),i.containerPosition=f.position(),i.containerSiz
e={height:f.innerHeight()-l[3],width:f.innerWidth()-l[1]};var c=i.containerOffset,h=i.containerSize.height,p=i.containerSize.width,d=e.ui.hasScroll(a,"left")?a.scrollWidth:p,v=e.ui.hasScroll(a)?a.scrollHeight:h;i.parentData={element:a,left:c.left,top:c.top,width:d,height:v}}},resize:function(t,n){var r=e(this).data("resizable"),i=r.options,s=r.containerSize,o=r.containerOffset,u=r.size,a=r.position,f=r._aspectRatio||t.shiftKey,l={top:0,left:0},c=r.containerElement;c[0]!=document&&/static/.test(c.css("position"))&&(l=o),a.left<(r._helper?o.left:0)&&(r.size.width=r.size.width+(r._helper?r.position.left-o.left:r.position.left-l.left),f&&(r.size.height=r.size.width/r.aspectRatio),r.position.left=i.helper?o.left:0),a.top<(r._helper?o.top:0)&&(r.size.height=r.size.height+(r._helper?r.position.top-o.top:r.position.top),f&&(r.size.width=r.size.height*r.aspectRatio),r.position.top=r._helper?o.top:0),r.offset.left=r.parentData.left+r.position.left,r.offset.top=r.parentData.top+r.posit
ion.top;var h=Math.abs((r._helper?r.offset.left-l.left:r.offset.left-l.left)+r.sizeDiff.width),p=Math.abs((r._helper?r.offset.top-l.top:r.offset.top-o.top)+r.sizeDiff.height),d=r.containerElement.get(0)==r.element.parent().get(0),v=/relative|absolute/.test(r.containerElement.css("position"));d&&v&&(h-=r.parentData.left),h+r.size.width>=r.parentData.width&&(r.size.width=r.parentData.width-h,f&&(r.size.height=r.size.width/r.aspectRatio)),p+r.size.height>=r.parentData.height&&(r.size.height=r.parentData.height-p,f&&(r.size.width=r.size.height*r.aspectRatio))},stop:function(t,n){var r=e(this).data("resizable"),i=r.options,s=r.position,o=r.containerOffset,u=r.containerPosition,a=r.containerElement,f=e(r.helper),l=f.offset(),c=f.outerWidth()-r.sizeDiff.width,h=f.outerHeight()-r.sizeDiff.height;r._helper&&!i.animate&&/relative/.test(a.css("position"))&&e(this).css({left:l.left-u.left-o.left,width:c,height:h}),r._helper&&!i.animate&&/static/.test(a.css("position"))&&e(this).css({lef
t:l.left-u.left-o.left,width:c,height:h})}}),e.ui.plugin.add("resizable","ghost",{start:function(t,n){var r=e(this).data("resizable"),i=r.options,s=r.size;r.ghost=r.originalElement.clone(),r.ghost.css({opacity:.25,display:"block",position:"relative",height:s.height,width:s.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof i.ghost=="string"?i.ghost:""),r.ghost.appendTo(r.helper)},resize:function(t,n){var r=e(this).data("resizable"),i=r.options;r.ghost&&r.ghost.css({position:"relative",height:r.size.height,width:r.size.width})},stop:function(t,n){var r=e(this).data("resizable"),i=r.options;r.ghost&&r.helper&&r.helper.get(0).removeChild(r.ghost.get(0))}}),e.ui.plugin.add("resizable","grid",{resize:function(t,n){var r=e(this).data("resizable"),i=r.options,s=r.size,o=r.originalSize,u=r.originalPosition,a=r.axis,f=i._aspectRatio||t.shiftKey;i.grid=typeof i.grid=="number"?[i.grid,i.grid]:i.grid;var l=Math.round((s.width-o.width)/(i.grid[0]||1))*(i.grid[0]
||1),c=Math.round((s.height-o.height)/(i.grid[1]||1))*(i.grid[1]||1);/^(se|s|e)$/.test(a)?(r.size.width=o.width+l,r.size.height=o.height+c):/^(ne)$/.test(a)?(r.size.width=o.width+l,r.size.height=o.height+c,r.position.top=u.top-c):/^(sw)$/.test(a)?(r.size.width=o.width+l,r.size.height=o.height+c,r.position.left=u.left-l):(r.size.width=o.width+l,r.size.height=o.height+c,r.position.top=u.top-c,r.position.left=u.left-l)}});var n=function(e){return parseInt(e,10)||0},r=function(e){return!isNaN(parseInt(e,10))}})(jQuery);(function(e,t){var n=0,r={},i={};r.height=r.paddingTop=r.paddingBottom=r.borderTopWidth=r.borderBottomWidth="hide",i.height=i.paddingTop=i.paddingBottom=i.borderTopWidth=i.borderBottomWidth="show",e.widget("ui.accordion",{version:"1.9.2",options:{active:0,animate:{},collapsible:!1,event:"click",header:"> li > :first-child,> :not(li):even",heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null}
,_create:function(){var t=this.accordionId="ui-accordion-"+(this.element.attr("id")||++n),r=this.options;this.prevShow=this.prevHide=e(),this.element.addClass("ui-accordion ui-widget ui-helper-reset"),this.headers=this.element.find(r.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all"),this._hoverable(this.headers),this._focusable(this.headers),this.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom").hide(),!r.collapsible&&(r.active===!1||r.active==null)&&(r.active=0),r.active<0&&(r.active+=this.headers.length),this.active=this._findActive(r.active).addClass("ui-accordion-header-active ui-state-active").toggleClass("ui-corner-all ui-corner-top"),this.active.next().addClass("ui-accordion-content-active").show(),this._createIcons(),this.refresh(),this.element.attr("role","tablist"),this.headers.attr("role","tab").each(function(n){var r=e(this),i=r.attr("id"),s=r.next(),o=s.attr("id");i||(i=t+"-hea
der-"+n,r.attr("id",i)),o||(o=t+"-panel-"+n,s.attr("id",o)),r.attr("aria-controls",o),s.attr("aria-labelledby",i)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false",tabIndex:-1}).next().attr({"aria-expanded":"false","aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true",tabIndex:0}).next().attr({"aria-expanded":"true","aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._on(this.headers,{keydown:"_keydown"}),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._setupEvents(r.event)},_getCreateEventData:function(){return{header:this.active,content:this.active.length?this.active.next():e()}},_createIcons:function(){var t=this.options.icons;t&&(e("<span>").addClass("ui-accordion-header-icon ui-icon "+t.header).prependTo(this.headers),this.active.children(".ui-accordion-header-icon").removeClass(t.header).addClass(t.activeHeader),this.headers.addClass("ui-accordion-icons"))},_destr
oyIcons:function(){this.headers.removeClass("ui-accordion-icons").children(".ui-accordion-header-icon").remove()},_destroy:function(){var e;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role"),this.headers.removeClass("ui-accordion-header ui-accordion-header-active ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-selected").removeAttr("aria-controls").removeAttr("tabIndex").each(function(){/^ui-accordion/.test(this.id)&&this.removeAttribute("id")}),this._destroyIcons(),e=this.headers.next().css("display","").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-labelledby").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-state-disabled").each(function(){/^ui-accordion/.test(this.id)&&this.removeAttribute("id")}),this.options.heightStyle!=="content"&&e.css("h
eight","")},_setOption:function(e,t){if(e==="active"){this._activate(t);return}e==="event"&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(t)),this._super(e,t),e==="collapsible"&&!t&&this.options.active===!1&&this._activate(0),e==="icons"&&(this._destroyIcons(),t&&this._createIcons()),e==="disabled"&&this.headers.add(this.headers.next()).toggleClass("ui-state-disabled",!!t)},_keydown:function(t){if(t.altKey||t.ctrlKey)return;var n=e.ui.keyCode,r=this.headers.length,i=this.headers.index(t.target),s=!1;switch(t.keyCode){case n.RIGHT:case n.DOWN:s=this.headers[(i+1)%r];break;case n.LEFT:case n.UP:s=this.headers[(i-1+r)%r];break;case n.SPACE:case n.ENTER:this._eventHandler(t);break;case n.HOME:s=this.headers[0];break;case n.END:s=this.headers[r-1]}s&&(e(t.target).attr("tabIndex",-1),e(s).attr("tabIndex",0),s.focus(),t.preventDefault())},_panelKeyDown:function(t){t.keyCode===e.ui.keyCode.UP&&t.ctrlKey&&e(t.currentTarget).prev().focus()},refresh:
function(){var t,n,r=this.options.heightStyle,i=this.element.parent();r==="fill"?(e.support.minHeight||(n=i.css("overflow"),i.css("overflow","hidden")),t=i.height(),this.element.siblings(":visible").each(function(){var n=e(this),r=n.css("position");if(r==="absolute"||r==="fixed")return;t-=n.outerHeight(!0)}),n&&i.css("overflow",n),this.headers.each(function(){t-=e(this).outerHeight(!0)}),this.headers.next().each(function(){e(this).height(Math.max(0,t-e(this).innerHeight()+e(this).height()))}).css("overflow","auto")):r==="auto"&&(t=0,this.headers.next().each(function(){t=Math.max(t,e(this).css("height","").height())}).height(t))},_activate:function(t){var n=this._findActive(t)[0];if(n===this.active[0])return;n=n||this.active[0],this._eventHandler({target:n,currentTarget:n,preventDefault:e.noop})},_findActive:function(t){return typeof t=="number"?this.headers.eq(t):e()},_setupEvents:function(t){var n={};if(!t)return;e.each(t.split(" "),function(e,t){n[t]="_eventHandler"}),this
._on(this.headers,n)},_eventHandler:function(t){var n=this.options,r=this.active,i=e(t.currentTarget),s=i[0]===r[0],o=s&&n.collapsible,u=o?e():i.next(),a=r.next(),f={oldHeader:r,oldPanel:a,newHeader:o?e():i,newPanel:u};t.preventDefault();if(s&&!n.collapsible||this._trigger("beforeActivate",t,f)===!1)return;n.active=o?!1:this.headers.index(i),this.active=s?e():i,this._toggle(f),r.removeClass("ui-accordion-header-active ui-state-active"),n.icons&&r.children(".ui-accordion-header-icon").removeClass(n.icons.activeHeader).addClass(n.icons.header),s||(i.removeClass("ui-corner-all").addClass("ui-accordion-header-active ui-state-active ui-corner-top"),n.icons&&i.children(".ui-accordion-header-icon").removeClass(n.icons.header).addClass(n.icons.activeHeader),i.next().addClass("ui-accordion-content-active"))},_toggle:function(t){var n=t.newPanel,r=this.prevShow.length?this.prevShow:t.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=n,this.prevHide=r,this.options.ani
mate?this._animate(n,r,t):(r.hide(),n.show(),this._toggleComplete(t)),r.attr({"aria-expanded":"false","aria-hidden":"true"}),r.prev().attr("aria-selected","false"),n.length&&r.length?r.prev().attr("tabIndex",-1):n.length&&this.headers.filter(function(){return e(this).attr("tabIndex")===0}).attr("tabIndex",-1),n.attr({"aria-expanded":"true","aria-hidden":"false"}).prev().attr({"aria-selected":"true",tabIndex:0})},_animate:function(e,t,n){var s,o,u,a=this,f=0,l=e.length&&(!t.length||e.index()<t.index()),c=this.options.animate||{},h=l&&c.down||c,p=function(){a._toggleComplete(n)};typeof h=="number"&&(u=h),typeof h=="string"&&(o=h),o=o||h.easing||c.easing,u=u||h.duration||c.duration;if(!t.length)return e.animate(i,u,o,p);if(!e.length)return t.animate(r,u,o,p);s=e.show().outerHeight(),t.animate(r,{duration:u,easing:o,step:function(e,t){t.now=Math.round(e)}}),e.hide().animate(i,{duration:u,easing:o,complete:p,step:function(e,n){n.now=Math.round(e),n.prop!=="height"?f+=n.now:a.opti
ons.heightStyle!=="content"&&(n.now=Math.round(s-t.outerHeight()-f),f=0)}})},_toggleComplete:function(e){var t=e.oldPanel;t.removeClass("ui-accordion-content-active").prev().removeClass("ui-corner-top").addClass("ui-corner-all"),t.length&&(t.parent()[0].className=t.parent()[0].className),this._trigger("activate",null,e)}}),e.uiBackCompat!==!1&&(function(e,t){e.extend(t.options,{navigation:!1,navigationFilter:function(){return this.href.toLowerCase()===location.href.toLowerCase()}});var n=t._create;t._create=function(){if(this.options.navigation){var t=this,r=this.element.find(this.options.header),i=r.next(),s=r.add(i).find("a").filter(this.options.navigationFilter)[0];s&&r.add(i).each(function(n){if(e.contains(this,s))return t.options.active=Math.floor(n/2),!1})}n.call(this)}}(jQuery,jQuery.ui.accordion.prototype),function(e,t){e.extend(t.options,{heightStyle:null,autoHeight:!0,clearStyle:!1,fillSpace:!1});var n=t._create,r=t._setOption;e.extend(t,{_create:function(){this.op
tions.heightStyle=this.options.heightStyle||this._mergeHeightStyle(),n.call(this)},_setOption:function(e){if(e==="autoHeight"||e==="clearStyle"||e==="fillSpace")this.options.heightStyle=this._mergeHeightStyle();r.apply(this,arguments)},_mergeHeightStyle:function(){var e=this.options;if(e.fillSpace)return"fill";if(e.clearStyle)return"content";if(e.autoHeight)return"auto"}})}(jQuery,jQuery.ui.accordion.prototype),function(e,t){e.extend(t.options.icons,{activeHeader:null,headerSelected:"ui-icon-triangle-1-s"});var n=t._createIcons;t._createIcons=function(){this.options.icons&&(this.options.icons.activeHeader=this.options.icons.activeHeader||this.options.icons.headerSelected),n.call(this)}}(jQuery,jQuery.ui.accordion.prototype),function(e,t){t.activate=t._activate;var n=t._findActive;t._findActive=function(e){return e===-1&&(e=!1),e&&typeof e!="number"&&(e=this.headers.index(this.headers.filter(e)),e===-1&&(e=!1)),n.call(this,e)}}(jQuery,jQuery.ui.accordion.prototype),jQuery.ui.
accordion.prototype.resize=jQuery.ui.accordion.prototype.refresh,function(e,t){e.extend(t.options,{change:null,changestart:null});var n=t._trigger;t._trigger=function(e,t,r){var i=n.apply(this,arguments);return i?(e==="beforeActivate"?i=n.call(this,"changestart",t,{oldHeader:r.oldHeader,oldContent:r.oldPanel,newHeader:r.newHeader,newContent:r.newPanel}):e==="activate"&&(i=n.call(this,"change",t,{oldHeader:r.oldHeader,oldContent:r.oldPanel,newHeader:r.newHeader,newContent:r.newPanel})),i):!1}}(jQuery,jQuery.ui.accordion.prototype),function(e,t){e.extend(t.options,{animate:null,animated:"slide"});var n=t._create;t._create=function(){var e=this.options;e.animate===null&&(e.animated?e.animated==="slide"?e.animate=300:e.animated==="bounceslide"?e.animate={duration:200,down:{easing:"easeOutBounce",duration:1e3}}:e.animate=e.animated:e.animate=!1),n.call(this)}}(jQuery,jQuery.ui.accordion.prototype))})(jQuery);(function(e,t){var n,r,i,s,o="ui-button ui-widget ui-state-default ui-co
rner-all",u="ui-state-hover ui-state-active ",a="ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only",f=function(){var t=e(this).find(":ui-button");setTimeout(function(){t.button("refresh")},1)},l=function(t){var n=t.name,r=t.form,i=e([]);return n&&(r?i=e(r).find("[name='"+n+"']"):i=e("[name='"+n+"']",t.ownerDocument).filter(function(){return!this.form})),i};e.widget("ui.button",{version:"1.9.2",defaultElement:"<button>",options:{disabled:null,text:!0,label:null,icons:{primary:null,secondary:null}},_create:function(){this.element.closest("form").unbind("reset"+this.eventNamespace).bind("reset"+this.eventNamespace,f),typeof this.options.disabled!="boolean"?this.options.disabled=!!this.element.prop("disabled"):this.element.prop("disabled",this.options.disabled),this._determineButtonType(),this.hasTitle=!!this.buttonElement.attr("title");var t=this,u=this.options,a=this.type==="checkbox"||th
is.type==="radio",c=a?"":"ui-state-active",h="ui-state-focus";u.label===null&&(u.label=this.type==="input"?this.buttonElement.val():this.buttonElement.html()),this._hoverable(this.buttonElement),this.buttonElement.addClass(o).attr("role","button").bind("mouseenter"+this.eventNamespace,function(){if(u.disabled)return;this===n&&e(this).addClass("ui-state-active")}).bind("mouseleave"+this.eventNamespace,function(){if(u.disabled)return;e(this).removeClass(c)}).bind("click"+this.eventNamespace,function(e){u.disabled&&(e.preventDefault(),e.stopImmediatePropagation())}),this.element.bind("focus"+this.eventNamespace,function(){t.buttonElement.addClass(h)}).bind("blur"+this.eventNamespace,function(){t.buttonElement.removeClass(h)}),a&&(this.element.bind("change"+this.eventNamespace,function(){if(s)return;t.refresh()}),this.buttonElement.bind("mousedown"+this.eventNamespace,function(e){if(u.disabled)return;s=!1,r=e.pageX,i=e.pageY}).bind("mouseup"+this.eventNamespace,function(e){if(u.
disabled)return;if(r!==e.pageX||i!==e.pageY)s=!0})),this.type==="checkbox"?this.buttonElement.bind("click"+this.eventNamespace,function(){if(u.disabled||s)return!1;e(this).toggleClass("ui-state-active"),t.buttonElement.attr("aria-pressed",t.element[0].checked)}):this.type==="radio"?this.buttonElement.bind("click"+this.eventNamespace,function(){if(u.disabled||s)return!1;e(this).addClass("ui-state-active"),t.buttonElement.attr("aria-pressed","true");var n=t.element[0];l(n).not(n).map(function(){return e(this).button("widget")[0]}).removeClass("ui-state-active").attr("aria-pressed","false")}):(this.buttonElement.bind("mousedown"+this.eventNamespace,function(){if(u.disabled)return!1;e(this).addClass("ui-state-active"),n=this,t.document.one("mouseup",function(){n=null})}).bind("mouseup"+this.eventNamespace,function(){if(u.disabled)return!1;e(this).removeClass("ui-state-active")}).bind("keydown"+this.eventNamespace,function(t){if(u.disabled)return!1;(t.keyCode===e.ui.keyCode.SPACE
||t.keyCode===e.ui.keyCode.ENTER)&&e(this).addClass("ui-state-active")}).bind("keyup"+this.eventNamespace,function(){e(this).removeClass("ui-state-active")}),this.buttonElement.is("a")&&this.buttonElement.keyup(function(t){t.keyCode===e.ui.keyCode.SPACE&&e(this).click()})),this._setOption("disabled",u.disabled),this._resetButton()},_determineButtonType:function(){var e,t,n;this.element.is("[type=checkbox]")?this.type="checkbox":this.element.is("[type=radio]")?this.type="radio":this.element.is("input")?this.type="input":this.type="button",this.type==="checkbox"||this.type==="radio"?(e=this.element.parents().last(),t="label[for='"+this.element.attr("id")+"']",this.buttonElement=e.find(t),this.buttonElement.length||(e=e.length?e.siblings():this.element.siblings(),this.buttonElement=e.filter(t),this.buttonElement.length||(this.buttonElement=e.find(t))),this.element.addClass("ui-helper-hidden-accessible"),n=this.element.is(":checked"),n&&this.buttonElement.addClass("ui-state-acti
ve"),this.buttonElement.prop("aria-pressed",n)):this.buttonElement=this.element},widget:function(){return this.buttonElement},_destroy:function(){this.element.removeClass("ui-helper-hidden-accessible"),this.buttonElement.removeClass(o+" "+u+" "+a).removeAttr("role").removeAttr("aria-pressed").html(this.buttonElement.find(".ui-button-text").html()),this.hasTitle||this.buttonElement.removeAttr("title")},_setOption:function(e,t){this._super(e,t);if(e==="disabled"){t?this.element.prop("disabled",!0):this.element.prop("disabled",!1);return}this._resetButton()},refresh:function(){var t=this.element.is("input, button")?this.element.is(":disabled"):this.element.hasClass("ui-button-disabled");t!==this.options.disabled&&this._setOption("disabled",t),this.type==="radio"?l(this.element[0]).each(function(){e(this).is(":checked")?e(this).button("widget").addClass("ui-state-active").attr("aria-pressed","true"):e(this).button("widget").removeClass("ui-state-active").attr("aria-pressed","fal
se")}):this.type==="checkbox"&&(this.element.is(":checked")?this.buttonElement.addClass("ui-state-active").attr("aria-pressed","true"):this.buttonElement.removeClass("ui-state-active").attr("aria-pressed","false"))},_resetButton:function(){if(this.type==="input"){this.options.label&&this.element.val(this.options.label);return}var t=this.buttonElement.removeClass(a),n=e("<span></span>",this.document[0]).addClass("ui-button-text").html(this.options.label).appendTo(t.empty()).text(),r=this.options.icons,i=r.primary&&r.secondary,s=[];r.primary||r.secondary?(this.options.text&&s.push("ui-button-text-icon"+(i?"s":r.primary?"-primary":"-secondary")),r.primary&&t.prepend("<span class='ui-button-icon-primary ui-icon "+r.primary+"'></span>"),r.secondary&&t.append("<span class='ui-button-icon-secondary ui-icon "+r.secondary+"'></span>"),this.options.text||(s.push(i?"ui-button-icons-only":"ui-button-icon-only"),this.hasTitle||t.attr("title",e.trim(n)))):s.push("ui-button-text-only"),t.a
ddClass(s.join(" "))}}),e.widget("ui.buttonset",{version:"1.9.2",options:{items:"button, input[type=button], input[type=submit], input[type=reset], input[type=checkbox], input[type=radio], a, :data(button)"},_create:function(){this.element.addClass("ui-buttonset")},_init:function(){this.refresh()},_setOption:function(e,t){e==="disabled"&&this.buttons.button("option",e,t),this._super(e,t)},refresh:function(){var t=this.element.css("direction")==="rtl";this.buttons=this.element.find(this.options.items).filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return e(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":first").addClass(t?"ui-corner-right":"ui-corner-left").end().filter(":last").addClass(t?"ui-corner-left":"ui-corner-right").end().end()},_destroy:function(){this.element.removeClass("ui-buttonset"),this.buttons.map(function(){return e(this).button("widget")[0]}).removeClass("ui-corne
r-left ui-corner-right").end().button("destroy")}})})(jQuery);(function($,undefined){function Datepicker(){this.debug=!1,this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday
","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},$.extend(this._defaults,this.regional[""]),this.dpDiv=bindHover($('<div id="'+this._mainDivId+'" class="ui-dat
epicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>'))}function bindHover(e){var t="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return e.delegate(t,"mouseout",function(){$(this).removeClass("ui-state-hover"),this.className.indexOf("ui-datepicker-prev")!=-1&&$(this).removeClass("ui-datepicker-prev-hover"),this.className.indexOf("ui-datepicker-next")!=-1&&$(this).removeClass("ui-datepicker-next-hover")}).delegate(t,"mouseover",function(){$.datepicker._isDisabledDatepicker(instActive.inline?e.parent()[0]:instActive.input[0])||($(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),$(this).addClass("ui-state-hover"),this.className.indexOf("ui-datepicker-prev")!=-1&&$(this).addClass("ui-datepicker-prev-hover"),this.className.indexOf("ui-datepicker-next")!=-1&&$(this).addClass("ui-datepicker-next-hover"))})}function extendRemove(e,t){$.extend(e,t);for(var n in t)if(t[n]==null||t[n]==undefined)
e[n]=t[n];return e}$.extend($.ui,{datepicker:{version:"1.9.2"}});var PROP_NAME="datepicker",dpuuid=(new Date).getTime(),instActive;$.extend(Datepicker.prototype,{markerClassName:"hasDatepicker",maxRows:4,log:function(){this.debug&&console.log.apply("",arguments)},_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(e){return extendRemove(this._defaults,e||{}),this},_attachDatepicker:function(target,settings){var inlineSettings=null;for(var attrName in this._defaults){var attrValue=target.getAttribute("date:"+attrName);if(attrValue){inlineSettings=inlineSettings||{};try{inlineSettings[attrName]=eval(attrValue)}catch(err){inlineSettings[attrName]=attrValue}}}var nodeName=target.nodeName.toLowerCase(),inline=nodeName=="div"||nodeName=="span";target.id||(this.uuid+=1,target.id="dp"+this.uuid);var inst=this._newInst($(target),inline);inst.settings=$.extend({},settings||{},inlineSettings||{}),nodeName=="input"?this._connectDatepicker(target,inst):inline&&this._inli
neDatepicker(target,inst)},_newInst:function(e,t){var n=e[0].id.replace(/([^A-Za-z0-9_-])/g,"\\\\$1");return{id:n,input:e,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:t,dpDiv:t?bindHover($('<div class="'+this._inlineClass+' ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>')):this.dpDiv}},_connectDatepicker:function(e,t){var n=$(e);t.append=$([]),t.trigger=$([]);if(n.hasClass(this.markerClassName))return;this._attachments(n,t),n.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker",function(e,n,r){t.settings[n]=r}).bind("getData.datepicker",function(e,n){return this._get(t,n)}),this._autoSize(t),$.data(e,PROP_NAME,t),t.settings.disabled&&this._disableDatepicker(e)},_attachments:function(e,t){var n=this._get(t,"appendText"),r=this._get(t,"isRTL");t.append&&t.append.remove(),n&&(t.append=$('<span class="'+this._appendClass+'">'+n+"</span>")
,e[r?"before":"after"](t.append)),e.unbind("focus",this._showDatepicker),t.trigger&&t.trigger.remove();var i=this._get(t,"showOn");(i=="focus"||i=="both")&&e.focus(this._showDatepicker);if(i=="button"||i=="both"){var s=this._get(t,"buttonText"),o=this._get(t,"buttonImage");t.trigger=$(this._get(t,"buttonImageOnly")?$("<img/>").addClass(this._triggerClass).attr({src:o,alt:s,title:s}):$('<button type="button"></button>').addClass(this._triggerClass).html(o==""?s:$("<img/>").attr({src:o,alt:s,title:s}))),e[r?"before":"after"](t.trigger),t.trigger.click(function(){return $.datepicker._datepickerShowing&&$.datepicker._lastInput==e[0]?$.datepicker._hideDatepicker():$.datepicker._datepickerShowing&&$.datepicker._lastInput!=e[0]?($.datepicker._hideDatepicker(),$.datepicker._showDatepicker(e[0])):$.datepicker._showDatepicker(e[0]),!1})}},_autoSize:function(e){if(this._get(e,"autoSize")&&!e.inline){var t=new Date(2009,11,20),n=this._get(e,"dateFormat");if(n.match(/[DM]/)){var r=functi
on(e){var t=0,n=0;for(var r=0;r<e.length;r++)e[r].length>t&&(t=e[r].length,n=r);return n};t.setMonth(r(this._get(e,n.match(/MM/)?"monthNames":"monthNamesShort"))),t.setDate(r(this._get(e,n.match(/DD/)?"dayNames":"dayNamesShort"))+20-t.getDay())}e.input.attr("size",this._formatDate(e,t).length)}},_inlineDatepicker:function(e,t){var n=$(e);if(n.hasClass(this.markerClassName))return;n.addClass(this.markerClassName).append(t.dpDiv).bind("setData.datepicker",function(e,n,r){t.settings[n]=r}).bind("getData.datepicker",function(e,n){return this._get(t,n)}),$.data(e,PROP_NAME,t),this._setDate(t,this._getDefaultDate(t),!0),this._updateDatepicker(t),this._updateAlternate(t),t.settings.disabled&&this._disableDatepicker(e),t.dpDiv.css("display","block")},_dialogDatepicker:function(e,t,n,r,i){var s=this._dialogInst;if(!s){this.uuid+=1;var o="dp"+this.uuid;this._dialogInput=$('<input type="text" id="'+o+'" style="position: absolute; top: -100px; width: 0px;"/>'),this._dialogInput.keydown(
this._doKeyDown),$("body").append(this._dialogInput),s=this._dialogInst=this._newInst(this._dialogInput,!1),s.settings={},$.data(this._dialogInput[0],PROP_NAME,s)}extendRemove(s.settings,r||{}),t=t&&t.constructor==Date?this._formatDate(s,t):t,this._dialogInput.val(t),this._pos=i?i.length?i:[i.pageX,i.pageY]:null;if(!this._pos){var u=document.documentElement.clientWidth,a=document.documentElement.clientHeight,f=document.documentElement.scrollLeft||document.body.scrollLeft,l=document.documentElement.scrollTop||document.body.scrollTop;this._pos=[u/2-100+f,a/2-150+l]}return this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),s.settings.onSelect=n,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),$.blockUI&&$.blockUI(this.dpDiv),$.data(this._dialogInput[0],PROP_NAME,s),this},_destroyDatepicker:function(e){var t=$(e),n=$.data(e,PROP_NAME);if(!t.hasClass(this.markerClassName))return;var r=e.nodeName.toLow
erCase();$.removeData(e,PROP_NAME),r=="input"?(n.append.remove(),n.trigger.remove(),t.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)):(r=="div"||r=="span")&&t.removeClass(this.markerClassName).empty()},_enableDatepicker:function(e){var t=$(e),n=$.data(e,PROP_NAME);if(!t.hasClass(this.markerClassName))return;var r=e.nodeName.toLowerCase();if(r=="input")e.disabled=!1,n.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""});else if(r=="div"||r=="span"){var i=t.children("."+this._inlineClass);i.children().removeClass("ui-state-disabled"),i.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!1)}this._disabledInputs=$.map(this._disabledInputs,function(t){return t==e?null:t})},_disableDatepicker:function(e){var t=$(e),n=$.data(e,PROP_NAME);if(!t.hasClass(this.markerClassName))re
turn;var r=e.nodeName.toLowerCase();if(r=="input")e.disabled=!0,n.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"});else if(r=="div"||r=="span"){var i=t.children("."+this._inlineClass);i.children().addClass("ui-state-disabled"),i.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!0)}this._disabledInputs=$.map(this._disabledInputs,function(t){return t==e?null:t}),this._disabledInputs[this._disabledInputs.length]=e},_isDisabledDatepicker:function(e){if(!e)return!1;for(var t=0;t<this._disabledInputs.length;t++)if(this._disabledInputs[t]==e)return!0;return!1},_getInst:function(e){try{return $.data(e,PROP_NAME)}catch(t){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(e,t,n){var r=this._getInst(e);if(arguments.length==2&&typeof t=="string")return t=="defaults"?$.extend({},$.datepicker._defaults):r?t=="all"?$.extend({},r.settings):this._get(r,t):null;var i=t
||{};typeof t=="string"&&(i={},i[t]=n);if(r){this._curInst==r&&this._hideDatepicker();var s=this._getDateDatepicker(e,!0),o=this._getMinMaxDate(r,"min"),u=this._getMinMaxDate(r,"max");extendRemove(r.settings,i),o!==null&&i.dateFormat!==undefined&&i.minDate===undefined&&(r.settings.minDate=this._formatDate(r,o)),u!==null&&i.dateFormat!==undefined&&i.maxDate===undefined&&(r.settings.maxDate=this._formatDate(r,u)),this._attachments($(e),r),this._autoSize(r),this._setDate(r,s),this._updateAlternate(r),this._updateDatepicker(r)}},_changeDatepicker:function(e,t,n){this._optionDatepicker(e,t,n)},_refreshDatepicker:function(e){var t=this._getInst(e);t&&this._updateDatepicker(t)},_setDateDatepicker:function(e,t){var n=this._getInst(e);n&&(this._setDate(n,t),this._updateDatepicker(n),this._updateAlternate(n))},_getDateDatepicker:function(e,t){var n=this._getInst(e);return n&&!n.inline&&this._setDateFromField(n,t),n?this._getDate(n):null},_doKeyDown:function(e){var t=$.datepicker._getI
nst(e.target),n=!0,r=t.dpDiv.is(".ui-datepicker-rtl");t._keyEvent=!0;if($.datepicker._datepickerShowing)switch(e.keyCode){case 9:$.datepicker._hideDatepicker(),n=!1;break;case 13:var i=$("td."+$.datepicker._dayOverClass+":not(."+$.datepicker._currentClass+")",t.dpDiv);i[0]&&$.datepicker._selectDay(e.target,t.selectedMonth,t.selectedYear,i[0]);var s=$.datepicker._get(t,"onSelect");if(s){var o=$.datepicker._formatDate(t);s.apply(t.input?t.input[0]:null,[o,t])}else $.datepicker._hideDatepicker();return!1;case 27:$.datepicker._hideDatepicker();break;case 33:$.datepicker._adjustDate(e.target,e.ctrlKey?-$.datepicker._get(t,"stepBigMonths"):-$.datepicker._get(t,"stepMonths"),"M");break;case 34:$.datepicker._adjustDate(e.target,e.ctrlKey?+$.datepicker._get(t,"stepBigMonths"):+$.datepicker._get(t,"stepMonths"),"M");break;case 35:(e.ctrlKey||e.metaKey)&&$.datepicker._clearDate(e.target),n=e.ctrlKey||e.metaKey;break;case 36:(e.ctrlKey||e.metaKey)&&$.datepicker._gotoToday(e.target),n=e.
ctrlKey||e.metaKey;break;case 37:(e.ctrlKey||e.metaKey)&&$.datepicker._adjustDate(e.target,r?1:-1,"D"),n=e.ctrlKey||e.metaKey,e.originalEvent.altKey&&$.datepicker._adjustDate(e.target,e.ctrlKey?-$.datepicker._get(t,"stepBigMonths"):-$.datepicker._get(t,"stepMonths"),"M");break;case 38:(e.ctrlKey||e.metaKey)&&$.datepicker._adjustDate(e.target,-7,"D"),n=e.ctrlKey||e.metaKey;break;case 39:(e.ctrlKey||e.metaKey)&&$.datepicker._adjustDate(e.target,r?-1:1,"D"),n=e.ctrlKey||e.metaKey,e.originalEvent.altKey&&$.datepicker._adjustDate(e.target,e.ctrlKey?+$.datepicker._get(t,"stepBigMonths"):+$.datepicker._get(t,"stepMonths"),"M");break;case 40:(e.ctrlKey||e.metaKey)&&$.datepicker._adjustDate(e.target,7,"D"),n=e.ctrlKey||e.metaKey;break;default:n=!1}else e.keyCode==36&&e.ctrlKey?$.datepicker._showDatepicker(this):n=!1;n&&(e.preventDefault(),e.stopPropagation())},_doKeyPress:function(e){var t=$.datepicker._getInst(e.target);if($.datepicker._get(t,"constrainInput")){var n=$.datepicker._p
ossibleChars($.datepicker._get(t,"dateFormat")),r=String.fromCharCode(e.charCode==undefined?e.keyCode:e.charCode);return e.ctrlKey||e.metaKey||r<" "||!n||n.indexOf(r)>-1}},_doKeyUp:function(e){var t=$.datepicker._getInst(e.target);if(t.input.val()!=t.lastVal)try{var n=$.datepicker.parseDate($.datepicker._get(t,"dateFormat"),t.input?t.input.val():null,$.datepicker._getFormatConfig(t));n&&($.datepicker._setDateFromField(t),$.datepicker._updateAlternate(t),$.datepicker._updateDatepicker(t))}catch(r){$.datepicker.log(r)}return!0},_showDatepicker:function(e){e=e.target||e,e.nodeName.toLowerCase()!="input"&&(e=$("input",e.parentNode)[0]);if($.datepicker._isDisabledDatepicker(e)||$.datepicker._lastInput==e)return;var t=$.datepicker._getInst(e);$.datepicker._curInst&&$.datepicker._curInst!=t&&($.datepicker._curInst.dpDiv.stop(!0,!0),t&&$.datepicker._datepickerShowing&&$.datepicker._hideDatepicker($.datepicker._curInst.input[0]));var n=$.datepicker._get(t,"beforeShow"),r=n?n.apply(e,
[e,t]):{};if(r===!1)return;extendRemove(t.settings,r),t.lastVal=null,$.datepicker._lastInput=e,$.datepicker._setDateFromField(t),$.datepicker._inDialog&&(e.value=""),$.datepicker._pos||($.datepicker._pos=$.datepicker._findPos(e),$.datepicker._pos[1]+=e.offsetHeight);var i=!1;$(e).parents().each(function(){return i|=$(this).css("position")=="fixed",!i});var s={left:$.datepicker._pos[0],top:$.datepicker._pos[1]};$.datepicker._pos=null,t.dpDiv.empty(),t.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),$.datepicker._updateDatepicker(t),s=$.datepicker._checkOffset(t,s,i),t.dpDiv.css({position:$.datepicker._inDialog&&$.blockUI?"static":i?"fixed":"absolute",display:"none",left:s.left+"px",top:s.top+"px"});if(!t.inline){var o=$.datepicker._get(t,"showAnim"),u=$.datepicker._get(t,"duration"),a=function(){var e=t.dpDiv.find("iframe.ui-datepicker-cover");if(!!e.length){var n=$.datepicker._getBorders(t.dpDiv);e.css({left:-n[0],top:-n[1],width:t.dpDiv.outerWidth(),height:t.
dpDiv.outerHeight()})}};t.dpDiv.zIndex($(e).zIndex()+1),$.datepicker._datepickerShowing=!0,$.effects&&($.effects.effect[o]||$.effects[o])?t.dpDiv.show(o,$.datepicker._get(t,"showOptions"),u,a):t.dpDiv[o||"show"](o?u:null,a),(!o||!u)&&a(),t.input.is(":visible")&&!t.input.is(":disabled")&&t.input.focus(),$.datepicker._curInst=t}},_updateDatepicker:function(e){this.maxRows=4;var t=$.datepicker._getBorders(e.dpDiv);instActive=e,e.dpDiv.empty().append(this._generateHTML(e)),this._attachHandlers(e);var n=e.dpDiv.find("iframe.ui-datepicker-cover");!n.length||n.css({left:-t[0],top:-t[1],width:e.dpDiv.outerWidth(),height:e.dpDiv.outerHeight()}),e.dpDiv.find("."+this._dayOverClass+" a").mouseover();var r=this._getNumberOfMonths(e),i=r[1],s=17;e.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),i>1&&e.dpDiv.addClass("ui-datepicker-multi-"+i).css("width",s*i+"em"),e.dpDiv[(r[0]!=1||r[1]!=1?"add":"remove")+"Class"]("ui-datepicker-multi"),e.d
pDiv[(this._get(e,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),e==$.datepicker._curInst&&$.datepicker._datepickerShowing&&e.input&&e.input.is(":visible")&&!e.input.is(":disabled")&&e.input[0]!=document.activeElement&&e.input.focus();if(e.yearshtml){var o=e.yearshtml;setTimeout(function(){o===e.yearshtml&&e.yearshtml&&e.dpDiv.find("select.ui-datepicker-year:first").replaceWith(e.yearshtml),o=e.yearshtml=null},0)}},_getBorders:function(e){var t=function(e){return{thin:1,medium:2,thick:3}[e]||e};return[parseFloat(t(e.css("border-left-width"))),parseFloat(t(e.css("border-top-width")))]},_checkOffset:function(e,t,n){var r=e.dpDiv.outerWidth(),i=e.dpDiv.outerHeight(),s=e.input?e.input.outerWidth():0,o=e.input?e.input.outerHeight():0,u=document.documentElement.clientWidth+(n?0:$(document).scrollLeft()),a=document.documentElement.clientHeight+(n?0:$(document).scrollTop());return t.left-=this._get(e,"isRTL")?r-s:0,t.left-=n&&t.left==e.input.offset().left?$(document).scrollL
eft():0,t.top-=n&&t.top==e.input.offset().top+o?$(document).scrollTop():0,t.left-=Math.min(t.left,t.left+r>u&&u>r?Math.abs(t.left+r-u):0),t.top-=Math.min(t.top,t.top+i>a&&a>i?Math.abs(i+o):0),t},_findPos:function(e){var t=this._getInst(e),n=this._get(t,"isRTL");while(e&&(e.type=="hidden"||e.nodeType!=1||$.expr.filters.hidden(e)))e=e[n?"previousSibling":"nextSibling"];var r=$(e).offset();return[r.left,r.top]},_hideDatepicker:function(e){var t=this._curInst;if(!t||e&&t!=$.data(e,PROP_NAME))return;if(this._datepickerShowing){var n=this._get(t,"showAnim"),r=this._get(t,"duration"),i=function(){$.datepicker._tidyDialog(t)};$.effects&&($.effects.effect[n]||$.effects[n])?t.dpDiv.hide(n,$.datepicker._get(t,"showOptions"),r,i):t.dpDiv[n=="slideDown"?"slideUp":n=="fadeIn"?"fadeOut":"hide"](n?r:null,i),n||i(),this._datepickerShowing=!1;var s=this._get(t,"onClose");s&&s.apply(t.input?t.input[0]:null,[t.input?t.input.val():"",t]),this._lastInput=null,this._inDialog&&(this._dialogInput.cs
s({position:"absolute",left:"0",top:"-100px"}),$.blockUI&&($.unblockUI(),$("body").append(this.dpDiv))),this._inDialog=!1}},_tidyDialog:function(e){e.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(e){if(!$.datepicker._curInst)return;var t=$(e.target),n=$.datepicker._getInst(t[0]);(t[0].id!=$.datepicker._mainDivId&&t.parents("#"+$.datepicker._mainDivId).length==0&&!t.hasClass($.datepicker.markerClassName)&&!t.closest("."+$.datepicker._triggerClass).length&&$.datepicker._datepickerShowing&&(!$.datepicker._inDialog||!$.blockUI)||t.hasClass($.datepicker.markerClassName)&&$.datepicker._curInst!=n)&&$.datepicker._hideDatepicker()},_adjustDate:function(e,t,n){var r=$(e),i=this._getInst(r[0]);if(this._isDisabledDatepicker(r[0]))return;this._adjustInstDate(i,t+(n=="M"?this._get(i,"showCurrentAtPos"):0),n),this._updateDatepicker(i)},_gotoToday:function(e){var t=$(e),n=this._getInst(t[0]);if(this._get(n,"gotoCurrent")&&n.currentDay)
n.selectedDay=n.currentDay,n.drawMonth=n.selectedMonth=n.currentMonth,n.drawYear=n.selectedYear=n.currentYear;else{var r=new Date;n.selectedDay=r.getDate(),n.drawMonth=n.selectedMonth=r.getMonth(),n.drawYear=n.selectedYear=r.getFullYear()}this._notifyChange(n),this._adjustDate(t)},_selectMonthYear:function(e,t,n){var r=$(e),i=this._getInst(r[0]);i["selected"+(n=="M"?"Month":"Year")]=i["draw"+(n=="M"?"Month":"Year")]=parseInt(t.options[t.selectedIndex].value,10),this._notifyChange(i),this._adjustDate(r)},_selectDay:function(e,t,n,r){var i=$(e);if($(r).hasClass(this._unselectableClass)||this._isDisabledDatepicker(i[0]))return;var s=this._getInst(i[0]);s.selectedDay=s.currentDay=$("a",r).html(),s.selectedMonth=s.currentMonth=t,s.selectedYear=s.currentYear=n,this._selectDate(e,this._formatDate(s,s.currentDay,s.currentMonth,s.currentYear))},_clearDate:function(e){var t=$(e),n=this._getInst(t[0]);this._selectDate(t,"")},_selectDate:function(e,t){var n=$(e),r=this._getInst(n[0]);t=
t!=null?t:this._formatDate(r),r.input&&r.input.val(t),this._updateAlternate(r);var i=this._get(r,"onSelect");i?i.apply(r.input?r.input[0]:null,[t,r]):r.input&&r.input.trigger("change"),r.inline?this._updateDatepicker(r):(this._hideDatepicker(),this._lastInput=r.input[0],typeof r.input[0]!="object"&&r.input.focus(),this._lastInput=null)},_updateAlternate:function(e){var t=this._get(e,"altField");if(t){var n=this._get(e,"altFormat")||this._get(e,"dateFormat"),r=this._getDate(e),i=this.formatDate(n,r,this._getFormatConfig(e));$(t).each(function(){$(this).val(i)})}},noWeekends:function(e){var t=e.getDay();return[t>0&&t<6,""]},iso8601Week:function(e){var t=new Date(e.getTime());t.setDate(t.getDate()+4-(t.getDay()||7));var n=t.getTime();return t.setMonth(0),t.setDate(1),Math.floor(Math.round((n-t)/864e5)/7)+1},parseDate:function(e,t,n){if(e==null||t==null)throw"Invalid arguments";t=typeof t=="object"?t.toString():t+"";if(t=="")return null;var r=(n?n.shortYearCutoff:null)||this._de
faults.shortYearCutoff;r=typeof r!="string"?r:(new Date).getFullYear()%100+parseInt(r,10);var i=(n?n.dayNamesShort:null)||this._defaults.dayNamesShort,s=(n?n.dayNames:null)||this._defaults.dayNames,o=(n?n.monthNamesShort:null)||this._defaults.monthNamesShort,u=(n?n.monthNames:null)||this._defaults.monthNames,a=-1,f=-1,l=-1,c=-1,h=!1,p=function(t){var n=y+1<e.length&&e.charAt(y+1)==t;return n&&y++,n},d=function(e){var n=p(e),r=e=="@"?14:e=="!"?20:e=="y"&&n?4:e=="o"?3:2,i=new RegExp("^\\d{1,"+r+"}"),s=t.substring(g).match(i);if(!s)throw"Missing number at position "+g;return g+=s[0].length,parseInt(s[0],10)},v=function(e,n,r){var i=$.map(p(e)?r:n,function(e,t){return[[t,e]]}).sort(function(e,t){return-(e[1].length-t[1].length)}),s=-1;$.each(i,function(e,n){var r=n[1];if(t.substr(g,r.length).toLowerCase()==r.toLowerCase())return s=n[0],g+=r.length,!1});if(s!=-1)return s+1;throw"Unknown name at position "+g},m=function(){if(t.charAt(g)!=e.charAt(y))throw"Unexpected literal at pos
ition "+g;g++},g=0;for(var y=0;y<e.length;y++)if(h)e.charAt(y)=="'"&&!p("'")?h=!1:m();else switch(e.charAt(y)){case"d":l=d("d");break;case"D":v("D",i,s);break;case"o":c=d("o");break;case"m":f=d("m");break;case"M":f=v("M",o,u);break;case"y":a=d("y");break;case"@":var b=new Date(d("@"));a=b.getFullYear(),f=b.getMonth()+1,l=b.getDate();break;case"!":var b=new Date((d("!")-this._ticksTo1970)/1e4);a=b.getFullYear(),f=b.getMonth()+1,l=b.getDate();break;case"'":p("'")?m():h=!0;break;default:m()}if(g<t.length){var w=t.substr(g);if(!/^\s+/.test(w))throw"Extra/unparsed characters found in date: "+w}a==-1?a=(new Date).getFullYear():a<100&&(a+=(new Date).getFullYear()-(new Date).getFullYear()%100+(a<=r?0:-100));if(c>-1){f=1,l=c;do{var E=this._getDaysInMonth(a,f-1);if(l<=E)break;f++,l-=E}while(!0)}var b=this._daylightSavingAdjust(new Date(a,f-1,l));if(b.getFullYear()!=a||b.getMonth()+1!=f||b.getDate()!=l)throw"Invalid date";return b},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd
",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925))*24*60*60*1e7,formatDate:function(e,t,n){if(!t)return"";var r=(n?n.dayNamesShort:null)||this._defaults.dayNamesShort,i=(n?n.dayNames:null)||this._defaults.dayNames,s=(n?n.monthNamesShort:null)||this._defaults.monthNamesShort,o=(n?n.monthNames:null)||this._defaults.monthNames,u=function(t){var n=h+1<e.length&&e.charAt(h+1)==t;return n&&h++,n},a=function(e,t,n){var r=""+t;if(u(e))while(r.length<n)r="0"+r;return r},f=function(e,t,n,r){return u(e)?r[t]:n[t]},l="",c=!1;if(t)for(var h=0;h<e.length;h++)if(c)e.charAt(h)=="'"&&!u("'")?c=!1:l+=e.charAt(h);else switch(e.charAt(h)){case"d":l+=a("d",t.getDate(),2);break;case"D":l+=f("D",t.getDay(),r,i);break;case"o":l+=a("o",Math.round(((new Date(t.getFullYear(),t.getMonth(),t.getDate())).getTime()-(new Date(t
.getFullYear(),0,0)).getTime())/864e5),3);break;case"m":l+=a("m",t.getMonth()+1,2);break;case"M":l+=f("M",t.getMonth(),s,o);break;case"y":l+=u("y")?t.getFullYear():(t.getYear()%100<10?"0":"")+t.getYear()%100;break;case"@":l+=t.getTime();break;case"!":l+=t.getTime()*1e4+this._ticksTo1970;break;case"'":u("'")?l+="'":c=!0;break;default:l+=e.charAt(h)}return l},_possibleChars:function(e){var t="",n=!1,r=function(t){var n=i+1<e.length&&e.charAt(i+1)==t;return n&&i++,n};for(var i=0;i<e.length;i++)if(n)e.charAt(i)=="'"&&!r("'")?n=!1:t+=e.charAt(i);else switch(e.charAt(i)){case"d":case"m":case"y":case"@":t+="0123456789";break;case"D":case"M":return null;case"'":r("'")?t+="'":n=!0;break;default:t+=e.charAt(i)}return t},_get:function(e,t){return e.settings[t]!==undefined?e.settings[t]:this._defaults[t]},_setDateFromField:function(e,t){if(e.input.val()==e.lastVal)return;var n=this._get(e,"dateFormat"),r=e.lastVal=e.input?e.input.val():null,i,s;i=s=this._getDefaultDate(e);var o=this._ge
tFormatConfig(e);try{i=this.parseDate(n,r,o)||s}catch(u){this.log(u),r=t?"":r}e.selectedDay=i.getDate(),e.drawMonth=e.selectedMonth=i.getMonth(),e.drawYear=e.selectedYear=i.getFullYear(),e.currentDay=r?i.getDate():0,e.currentMonth=r?i.getMonth():0,e.currentYear=r?i.getFullYear():0,this._adjustInstDate(e)},_getDefaultDate:function(e){return this._restrictMinMax(e,this._determineDate(e,this._get(e,"defaultDate"),new Date))},_determineDate:function(e,t,n){var r=function(e){var t=new Date;return t.setDate(t.getDate()+e),t},i=function(t){try{return $.datepicker.parseDate($.datepicker._get(e,"dateFormat"),t,$.datepicker._getFormatConfig(e))}catch(n){}var r=(t.toLowerCase().match(/^c/)?$.datepicker._getDate(e):null)||new Date,i=r.getFullYear(),s=r.getMonth(),o=r.getDate(),u=/([+-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,a=u.exec(t);while(a){switch(a[2]||"d"){case"d":case"D":o+=parseInt(a[1],10);break;case"w":case"W":o+=parseInt(a[1],10)*7;break;case"m":case"M":s+=parseInt(a[1],10),o=Math.mi
n(o,$.datepicker._getDaysInMonth(i,s));break;case"y":case"Y":i+=parseInt(a[1],10),o=Math.min(o,$.datepicker._getDaysInMonth(i,s))}a=u.exec(t)}return new Date(i,s,o)},s=t==null||t===""?n:typeof t=="string"?i(t):typeof t=="number"?isNaN(t)?n:r(t):new Date(t.getTime());return s=s&&s.toString()=="Invalid Date"?n:s,s&&(s.setHours(0),s.setMinutes(0),s.setSeconds(0),s.setMilliseconds(0)),this._daylightSavingAdjust(s)},_daylightSavingAdjust:function(e){return e?(e.setHours(e.getHours()>12?e.getHours()+2:0),e):null},_setDate:function(e,t,n){var r=!t,i=e.selectedMonth,s=e.selectedYear,o=this._restrictMinMax(e,this._determineDate(e,t,new Date));e.selectedDay=e.currentDay=o.getDate(),e.drawMonth=e.selectedMonth=e.currentMonth=o.getMonth(),e.drawYear=e.selectedYear=e.currentYear=o.getFullYear(),(i!=e.selectedMonth||s!=e.selectedYear)&&!n&&this._notifyChange(e),this._adjustInstDate(e),e.input&&e.input.val(r?"":this._formatDate(e))},_getDate:function(e){var t=!e.currentYear||e.input&&e.inp
ut.val()==""?null:this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return t},_attachHandlers:function(e){var t=this._get(e,"stepMonths"),n="#"+e.id.replace(/\\\\/g,"\\");e.dpDiv.find("[data-handler]").map(function(){var e={prev:function(){window["DP_jQuery_"+dpuuid].datepicker._adjustDate(n,-t,"M")},next:function(){window["DP_jQuery_"+dpuuid].datepicker._adjustDate(n,+t,"M")},hide:function(){window["DP_jQuery_"+dpuuid].datepicker._hideDatepicker()},today:function(){window["DP_jQuery_"+dpuuid].datepicker._gotoToday(n)},selectDay:function(){return window["DP_jQuery_"+dpuuid].datepicker._selectDay(n,+this.getAttribute("data-month"),+this.getAttribute("data-year"),this),!1},selectMonth:function(){return window["DP_jQuery_"+dpuuid].datepicker._selectMonthYear(n,this,"M"),!1},selectYear:function(){return window["DP_jQuery_"+dpuuid].datepicker._selectMonthYear(n,this,"Y"),!1}};$(this).bind(this.getAttribute("data-event"),e[this.getAttribute("data-hand
ler")])})},_generateHTML:function(e){var t=new Date;t=this._daylightSavingAdjust(new Date(t.getFullYear(),t.getMonth(),t.getDate()));var n=this._get(e,"isRTL"),r=this._get(e,"showButtonPanel"),i=this._get(e,"hideIfNoPrevNext"),s=this._get(e,"navigationAsDateFormat"),o=this._getNumberOfMonths(e),u=this._get(e,"showCurrentAtPos"),a=this._get(e,"stepMonths"),f=o[0]!=1||o[1]!=1,l=this._daylightSavingAdjust(e.currentDay?new Date(e.currentYear,e.currentMonth,e.currentDay):new Date(9999,9,9)),c=this._getMinMaxDate(e,"min"),h=this._getMinMaxDate(e,"max"),p=e.drawMonth-u,d=e.drawYear;p<0&&(p+=12,d--);if(h){var v=this._daylightSavingAdjust(new Date(h.getFullYear(),h.getMonth()-o[0]*o[1]+1,h.getDate()));v=c&&v<c?c:v;while(this._daylightSavingAdjust(new Date(d,p,1))>v)p--,p<0&&(p=11,d--)}e.drawMonth=p,e.drawYear=d;var m=this._get(e,"prevText");m=s?this.formatDate(m,this._daylightSavingAdjust(new Date(d,p-a,1)),this._getFormatConfig(e)):m;var g=this._canAdjustMonth(e,-1,d,p)?'<a class="u
i-datepicker-prev ui-corner-all" data-handler="prev" data-event="click" title="'+m+'"><span class="ui-icon ui-icon-circle-triangle-'+(n?"e":"w")+'">'+m+"</span></a>":i?"":'<a class="ui-datepicker-prev ui-corner-all ui-state-disabled" title="'+m+'"><span class="ui-icon ui-icon-circle-triangle-'+(n?"e":"w")+'">'+m+"</span></a>",y=this._get(e,"nextText");y=s?this.formatDate(y,this._daylightSavingAdjust(new Date(d,p+a,1)),this._getFormatConfig(e)):y;var b=this._canAdjustMonth(e,1,d,p)?'<a class="ui-datepicker-next ui-corner-all" data-handler="next" data-event="click" title="'+y+'"><span class="ui-icon ui-icon-circle-triangle-'+(n?"w":"e")+'">'+y+"</span></a>":i?"":'<a class="ui-datepicker-next ui-corner-all ui-state-disabled" title="'+y+'"><span class="ui-icon ui-icon-circle-triangle-'+(n?"w":"e")+'">'+y+"</span></a>",w=this._get(e,"currentText"),E=this._get(e,"gotoCurrent")&&e.currentDay?l:t;w=s?this.formatDate(w,E,this._getFormatConfig(e)):w;var S=e.inline?"":'<button type="bu
tton" class="ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all" data-handler="hide" data-event="click">'+this._get(e,"closeText")+"</button>",x=r?'<div class="ui-datepicker-buttonpane ui-widget-content">'+(n?S:"")+(this._isInRange(e,E)?'<button type="button" class="ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all" data-handler="today" data-event="click">'+w+"</button>":"")+(n?"":S)+"</div>":"",T=parseInt(this._get(e,"firstDay"),10);T=isNaN(T)?0:T;var N=this._get(e,"showWeek"),C=this._get(e,"dayNames"),k=this._get(e,"dayNamesShort"),L=this._get(e,"dayNamesMin"),A=this._get(e,"monthNames"),O=this._get(e,"monthNamesShort"),M=this._get(e,"beforeShowDay"),_=this._get(e,"showOtherMonths"),D=this._get(e,"selectOtherMonths"),P=this._get(e,"calculateWeek")||this.iso8601Week,H=this._getDefaultDate(e),B="";for(var j=0;j<o[0];j++){var F="";this.maxRows=4;for(var I=0;I<o[1];I++){var q=this._daylightSavingAdjust(new Date(d,p,e.selectedDay)),R
=" ui-corner-all",U="";if(f){U+='<div class="ui-datepicker-group';if(o[1]>1)switch(I){case 0:U+=" ui-datepicker-group-first",R=" ui-corner-"+(n?"right":"left");break;case o[1]-1:U+=" ui-datepicker-group-last",R=" ui-corner-"+(n?"left":"right");break;default:U+=" ui-datepicker-group-middle",R=""}U+='">'}U+='<div class="ui-datepicker-header ui-widget-header ui-helper-clearfix'+R+'">'+(/all|left/.test(R)&&j==0?n?b:g:"")+(/all|right/.test(R)&&j==0?n?g:b:"")+this._generateMonthYearHeader(e,p,d,c,h,j>0||I>0,A,O)+'</div><table class="ui-datepicker-calendar"><thead>'+"<tr>";var z=N?'<th class="ui-datepicker-week-col">'+this._get(e,"weekHeader")+"</th>":"";for(var W=0;W<7;W++){var X=(W+T)%7;z+="<th"+((W+T+6)%7>=5?' class="ui-datepicker-week-end"':"")+">"+'<span title="'+C[X]+'">'+L[X]+"</span></th>"}U+=z+"</tr></thead><tbody>";var V=this._getDaysInMonth(d,p);d==e.selectedYear&&p==e.selectedMonth&&(e.selectedDay=Math.min(e.selectedDay,V));var J=(this._getFirstDayOfMonth(d,p)-T+7)%7,K=
Math.ceil((J+V)/7),Q=f?this.maxRows>K?this.maxRows:K:K;this.maxRows=Q;var G=this._daylightSavingAdjust(new Date(d,p,1-J));for(var Y=0;Y<Q;Y++){U+="<tr>";var Z=N?'<td class="ui-datepicker-week-col">'+this._get(e,"calculateWeek")(G)+"</td>":"";for(var W=0;W<7;W++){var et=M?M.apply(e.input?e.input[0]:null,[G]):[!0,""],tt=G.getMonth()!=p,nt=tt&&!D||!et[0]||c&&G<c||h&&G>h;Z+='<td class="'+((W+T+6)%7>=5?" ui-datepicker-week-end":"")+(tt?" ui-datepicker-other-month":"")+(G.getTime()==q.getTime()&&p==e.selectedMonth&&e._keyEvent||H.getTime()==G.getTime()&&H.getTime()==q.getTime()?" "+this._dayOverClass:"")+(nt?" "+this._unselectableClass+" ui-state-disabled":"")+(tt&&!_?"":" "+et[1]+(G.getTime()==l.getTime()?" "+this._currentClass:"")+(G.getTime()==t.getTime()?" ui-datepicker-today":""))+'"'+((!tt||_)&&et[2]?' title="'+et[2]+'"':"")+(nt?"":' data-handler="selectDay" data-event="click" data-month="'+G.getMonth()+'" data-year="'+G.getFullYear()+'"')+">"+(tt&&!_?" ":nt?'<span clas
s="ui-state-default">'+G.getDate()+"</span>":'<a class="ui-state-default'+(G.getTime()==t.getTime()?" ui-state-highlight":"")+(G.getTime()==l.getTime()?" ui-state-active":"")+(tt?" ui-priority-secondary":"")+'" href="#">'+G.getDate()+"</a>")+"</td>",G.setDate(G.getDate()+1),G=this._daylightSavingAdjust(G)}U+=Z+"</tr>"}p++,p>11&&(p=0,d++),U+="</tbody></table>"+(f?"</div>"+(o[0]>0&&I==o[1]-1?'<div class="ui-datepicker-row-break"></div>':""):""),F+=U}B+=F}return B+=x+($.ui.ie6&&!e.inline?'<iframe src="javascript:false;" class="ui-datepicker-cover" frameborder="0"></iframe>':""),e._keyEvent=!1,B},_generateMonthYearHeader:function(e,t,n,r,i,s,o,u){var a=this._get(e,"changeMonth"),f=this._get(e,"changeYear"),l=this._get(e,"showMonthAfterYear"),c='<div class="ui-datepicker-title">',h="";if(s||!a)h+='<span class="ui-datepicker-month">'+o[t]+"</span>";else{var p=r&&r.getFullYear()==n,d=i&&i.getFullYear()==n;h+='<select class="ui-datepicker-month" data-handler="selectMonth" data-event
="change">';for(var v=0;v<12;v++)(!p||v>=r.getMonth())&&(!d||v<=i.getMonth())&&(h+='<option value="'+v+'"'+(v==t?' selected="selected"':"")+">"+u[v]+"</option>");h+="</select>"}l||(c+=h+(s||!a||!f?" ":""));if(!e.yearshtml){e.yearshtml="";if(s||!f)c+='<span class="ui-datepicker-year">'+n+"</span>";else{var m=this._get(e,"yearRange").split(":"),g=(new Date).getFullYear(),y=function(e){var t=e.match(/c[+-].*/)?n+parseInt(e.substring(1),10):e.match(/[+-].*/)?g+parseInt(e,10):parseInt(e,10);return isNaN(t)?g:t},b=y(m[0]),w=Math.max(b,y(m[1]||""));b=r?Math.max(b,r.getFullYear()):b,w=i?Math.min(w,i.getFullYear()):w,e.yearshtml+='<select class="ui-datepicker-year" data-handler="selectYear" data-event="change">';for(;b<=w;b++)e.yearshtml+='<option value="'+b+'"'+(b==n?' selected="selected"':"")+">"+b+"</option>";e.yearshtml+="</select>",c+=e.yearshtml,e.yearshtml=null}}return c+=this._get(e,"yearSuffix"),l&&(c+=(s||!a||!f?" ":"")+h),c+="</div>",c},_adjustInstDate:function(e
,t,n){var r=e.drawYear+(n=="Y"?t:0),i=e.drawMonth+(n=="M"?t:0),s=Math.min(e.selectedDay,this._getDaysInMonth(r,i))+(n=="D"?t:0),o=this._restrictMinMax(e,this._daylightSavingAdjust(new Date(r,i,s)));e.selectedDay=o.getDate(),e.drawMonth=e.selectedMonth=o.getMonth(),e.drawYear=e.selectedYear=o.getFullYear(),(n=="M"||n=="Y")&&this._notifyChange(e)},_restrictMinMax:function(e,t){var n=this._getMinMaxDate(e,"min"),r=this._getMinMaxDate(e,"max"),i=n&&t<n?n:t;return i=r&&i>r?r:i,i},_notifyChange:function(e){var t=this._get(e,"onChangeMonthYear");t&&t.apply(e.input?e.input[0]:null,[e.selectedYear,e.selectedMonth+1,e])},_getNumberOfMonths:function(e){var t=this._get(e,"numberOfMonths");return t==null?[1,1]:typeof t=="number"?[1,t]:t},_getMinMaxDate:function(e,t){return this._determineDate(e,this._get(e,t+"Date"),null)},_getDaysInMonth:function(e,t){return 32-this._daylightSavingAdjust(new Date(e,t,32)).getDate()},_getFirstDayOfMonth:function(e,t){return(new Date(e,t,1)).getDay()},_ca
nAdjustMonth:function(e,t,n,r){var i=this._getNumberOfMonths(e),s=this._daylightSavingAdjust(new Date(n,r+(t<0?t:i[0]*i[1]),1));return t<0&&s.setDate(this._getDaysInMonth(s.getFullYear(),s.getMonth())),this._isInRange(e,s)},_isInRange:function(e,t){var n=this._getMinMaxDate(e,"min"),r=this._getMinMaxDate(e,"max");return(!n||t.getTime()>=n.getTime())&&(!r||t.getTime()<=r.getTime())},_getFormatConfig:function(e){var t=this._get(e,"shortYearCutoff");return t=typeof t!="string"?t:(new Date).getFullYear()%100+parseInt(t,10),{shortYearCutoff:t,dayNamesShort:this._get(e,"dayNamesShort"),dayNames:this._get(e,"dayNames"),monthNamesShort:this._get(e,"monthNamesShort"),monthNames:this._get(e,"monthNames")}},_formatDate:function(e,t,n,r){t||(e.currentDay=e.selectedDay,e.currentMonth=e.selectedMonth,e.currentYear=e.selectedYear);var i=t?typeof t=="object"?t:this._daylightSavingAdjust(new Date(r,n,t)):this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return t
his.formatDate(this._get(e,"dateFormat"),i,this._getFormatConfig(e))}}),$.fn.datepicker=function(e){if(!this.length)return this;$.datepicker.initialized||($(document).mousedown($.datepicker._checkExternalClick).find(document.body).append($.datepicker.dpDiv),$.datepicker.initialized=!0);var t=Array.prototype.slice.call(arguments,1);return typeof e!="string"||e!="isDisabled"&&e!="getDate"&&e!="widget"?e=="option"&&arguments.length==2&&typeof arguments[1]=="string"?$.datepicker["_"+e+"Datepicker"].apply($.datepicker,[this[0]].concat(t)):this.each(function(){typeof e=="string"?$.datepicker["_"+e+"Datepicker"].apply($.datepicker,[this].concat(t)):$.datepicker._attachDatepicker(this,e)}):$.datepicker["_"+e+"Datepicker"].apply($.datepicker,[this[0]].concat(t))},$.datepicker=new Datepicker,$.datepicker.initialized=!1,$.datepicker.uuid=(new Date).getTime(),$.datepicker.version="1.9.2",window["DP_jQuery_"+dpuuid]=$})(jQuery);(function(e,t){var n="ui-dialog ui-widget ui-widget-content
ui-corner-all ",r={buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},i={maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0};e.widget("ui.dialog",{version:"1.9.2",options:{autoOpen:!0,buttons:{},closeOnEscape:!0,closeText:"close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:!1,maxWidth:!1,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",of:window,collision:"fit",using:function(t){var n=e(this).css(t).offset().top;n<0&&e(this).css("top",t.top-n)}},resizable:!0,show:null,stack:!0,title:"",width:300,zIndex:1e3},_create:function(){this.originalTitle=this.element.attr("title"),typeof this.originalTitle!="string"&&(this.originalTitle=""),this.oldPosition={parent:this.element.parent(),index:this.element.parent().children().index(this.element)},this.options.title=this.options.title||this.originalTitle;var t=this,r=this.options,i=r.title||" ",s,o,u,a,f;s=(this.uiDialog=e("<div>")).addClass(n+r.dialogClass).css({d
isplay:"none",outline:0,zIndex:r.zIndex}).attr("tabIndex",-1).keydown(function(n){r.closeOnEscape&&!n.isDefaultPrevented()&&n.keyCode&&n.keyCode===e.ui.keyCode.ESCAPE&&(t.close(n),n.preventDefault())}).mousedown(function(e){t.moveToTop(!1,e)}).appendTo("body"),this.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(s),o=(this.uiDialogTitlebar=e("<div>")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").bind("mousedown",function(){s.focus()}).prependTo(s),u=e("<a href='#'></a>").addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").click(function(e){e.preventDefault(),t.close(e)}).appendTo(o),(this.uiDialogTitlebarCloseText=e("<span>")).addClass("ui-icon ui-icon-closethick").text(r.closeText).appendTo(u),a=e("<span>").uniqueId().addClass("ui-dialog-title").html(i).prependTo(o),f=(this.uiDialogButtonPane=e("<div>")).addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix
"),(this.uiButtonSet=e("<div>")).addClass("ui-dialog-buttonset").appendTo(f),s.attr({role:"dialog","aria-labelledby":a.attr("id")}),o.find("*").add(o).disableSelection(),this._hoverable(u),this._focusable(u),r.draggable&&e.fn.draggable&&this._makeDraggable(),r.resizable&&e.fn.resizable&&this._makeResizable(),this._createButtons(r.buttons),this._isOpen=!1,e.fn.bgiframe&&s.bgiframe(),this._on(s,{keydown:function(t){if(!r.modal||t.keyCode!==e.ui.keyCode.TAB)return;var n=e(":tabbable",s),i=n.filter(":first"),o=n.filter(":last");if(t.target===o[0]&&!t.shiftKey)return i.focus(1),!1;if(t.target===i[0]&&t.shiftKey)return o.focus(1),!1}})},_init:function(){this.options.autoOpen&&this.open()},_destroy:function(){var e,t=this.oldPosition;this.overlay&&this.overlay.destroy(),this.uiDialog.hide(),this.element.removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body"),this.uiDialog.remove(),this.originalTitle&&this.element.attr("title",this.originalTitle),e=t.parent.childr
en().eq(t.index),e.length&&e[0]!==this.element[0]?e.before(this.element):t.parent.append(this.element)},widget:function(){return this.uiDialog},close:function(t){var n=this,r,i;if(!this._isOpen)return;if(!1===this._trigger("beforeClose",t))return;return this._isOpen=!1,this.overlay&&this.overlay.destroy(),this.options.hide?this._hide(this.uiDialog,this.options.hide,function(){n._trigger("close",t)}):(this.uiDialog.hide(),this._trigger("close",t)),e.ui.dialog.overlay.resize(),this.options.modal&&(r=0,e(".ui-dialog").each(function(){this!==n.uiDialog[0]&&(i=e(this).css("z-index"),isNaN(i)||(r=Math.max(r,i)))}),e.ui.dialog.maxZ=r),this},isOpen:function(){return this._isOpen},moveToTop:function(t,n){var r=this.options,i;return r.modal&&!t||!r.stack&&!r.modal?this._trigger("focus",n):(r.zIndex>e.ui.dialog.maxZ&&(e.ui.dialog.maxZ=r.zIndex),this.overlay&&(e.ui.dialog.maxZ+=1,e.ui.dialog.overlay.maxZ=e.ui.dialog.maxZ,this.overlay.$el.css("z-index",e.ui.dialog.overlay.maxZ)),i={scrol
lTop:this.element.scrollTop(),scrollLeft:this.element.scrollLeft()},e.ui.dialog.maxZ+=1,this.uiDialog.css("z-index",e.ui.dialog.maxZ),this.element.attr(i),this._trigger("focus",n),this)},open:function(){if(this._isOpen)return;var t,n=this.options,r=this.uiDialog;return this._size(),this._position(n.position),r.show(n.show),this.overlay=n.modal?new e.ui.dialog.overlay(this):null,this.moveToTop(!0),t=this.element.find(":tabbable"),t.length||(t=this.uiDialogButtonPane.find(":tabbable"),t.length||(t=r)),t.eq(0).focus(),this._isOpen=!0,this._trigger("open"),this},_createButtons:function(t){var n=this,r=!1;this.uiDialogButtonPane.remove(),this.uiButtonSet.empty(),typeof t=="object"&&t!==null&&e.each(t,function(){return!(r=!0)}),r?(e.each(t,function(t,r){var i,s;r=e.isFunction(r)?{click:r,text:t}:r,r=e.extend({type:"button"},r),s=r.click,r.click=function(){s.apply(n.element[0],arguments)},i=e("<button></button>",r).appendTo(n.uiButtonSet),e.fn.button&&i.button()}),this.uiDialog.add
Class("ui-dialog-buttons"),this.uiDialogButtonPane.appendTo(this.uiDialog)):this.uiDialog.removeClass("ui-dialog-buttons")},_makeDraggable:function(){function r(e){return{position:e.position,offset:e.offset}}var t=this,n=this.options;this.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(n,i){e(this).addClass("ui-dialog-dragging"),t._trigger("dragStart",n,r(i))},drag:function(e,n){t._trigger("drag",e,r(n))},stop:function(i,s){n.position=[s.position.left-t.document.scrollLeft(),s.position.top-t.document.scrollTop()],e(this).removeClass("ui-dialog-dragging"),t._trigger("dragStop",i,r(s)),e.ui.dialog.overlay.resize()}})},_makeResizable:function(n){function u(e){return{originalPosition:e.originalPosition,originalSize:e.originalSize,position:e.position,size:e.size}}n=n===t?this.options.resizable:n;var r=this,i=this.options,s=this.uiDialog.css("position"),o=typeof n=="string"?n:"n,e,s,w,se,
sw,ne,nw";this.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:this.element,maxWidth:i.maxWidth,maxHeight:i.maxHeight,minWidth:i.minWidth,minHeight:this._minHeight(),handles:o,start:function(t,n){e(this).addClass("ui-dialog-resizing"),r._trigger("resizeStart",t,u(n))},resize:function(e,t){r._trigger("resize",e,u(t))},stop:function(t,n){e(this).removeClass("ui-dialog-resizing"),i.height=e(this).height(),i.width=e(this).width(),r._trigger("resizeStop",t,u(n)),e.ui.dialog.overlay.resize()}}).css("position",s).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var e=this.options;return e.height==="auto"?e.minHeight:Math.min(e.minHeight,e.height)},_position:function(t){var n=[],r=[0,0],i;if(t){if(typeof t=="string"||typeof t=="object"&&"0"in t)n=t.split?t.split(" "):[t[0],t[1]],n.length===1&&(n[1]=n[0]),e.each(["left","top"],function(e,t){+n[e]===n[e]&&(r[e]=n[e],n[e]=t)}),t={my:n[0]+(r[0]<0?r[0]:"+"+r[0]
)+" "+n[1]+(r[1]<0?r[1]:"+"+r[1]),at:n.join(" ")};t=e.extend({},e.ui.dialog.prototype.options.position,t)}else t=e.ui.dialog.prototype.options.position;i=this.uiDialog.is(":visible"),i||this.uiDialog.show(),this.uiDialog.position(t),i||this.uiDialog.hide()},_setOptions:function(t){var n=this,s={},o=!1;e.each(t,function(e,t){n._setOption(e,t),e in r&&(o=!0),e in i&&(s[e]=t)}),o&&this._size(),this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option",s)},_setOption:function(t,r){var i,s,o=this.uiDialog;switch(t){case"buttons":this._createButtons(r);break;case"closeText":this.uiDialogTitlebarCloseText.text(""+r);break;case"dialogClass":o.removeClass(this.options.dialogClass).addClass(n+r);break;case"disabled":r?o.addClass("ui-dialog-disabled"):o.removeClass("ui-dialog-disabled");break;case"draggable":i=o.is(":data(draggable)"),i&&!r&&o.draggable("destroy"),!i&&r&&this._makeDraggable();break;case"position":this._position(r);break;case"resizable":s=o.is(":data(resizab
le)"),s&&!r&&o.resizable("destroy"),s&&typeof r=="string"&&o.resizable("option","handles",r),!s&&r!==!1&&this._makeResizable(r);break;case"title":e(".ui-dialog-title",this.uiDialogTitlebar).html(""+(r||" "))}this._super(t,r)},_size:function(){var t,n,r,i=this.options,s=this.uiDialog.is(":visible");this.element.show().css({width:"auto",minHeight:0,height:0}),i.minWidth>i.width&&(i.width=i.minWidth),t=this.uiDialog.css({height:"auto",width:i.width}).outerHeight(),n=Math.max(0,i.minHeight-t),i.height==="auto"?e.support.minHeight?this.element.css({minHeight:n,height:"auto"}):(this.uiDialog.show(),r=this.element.css("height","auto").height(),s||this.uiDialog.hide(),this.element.height(Math.max(r,n))):this.element.height(Math.max(i.height-t,0)),this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}}),e.extend(e.ui.dialog,{uuid:0,maxZ:0,getTitleId:function(e){var t=e.attr("id");return t||(this.uuid+=1,t=this.uuid),"ui-dialog-titl
e-"+t},overlay:function(t){this.$el=e.ui.dialog.overlay.create(t)}}),e.extend(e.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:e.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),function(e){return e+".dialog-overlay"}).join(" "),create:function(t){this.instances.length===0&&(setTimeout(function(){e.ui.dialog.overlay.instances.length&&e(document).bind(e.ui.dialog.overlay.events,function(t){if(e(t.target).zIndex()<e.ui.dialog.overlay.maxZ)return!1})},1),e(window).bind("resize.dialog-overlay",e.ui.dialog.overlay.resize));var n=this.oldInstances.pop()||e("<div>").addClass("ui-widget-overlay");return e(document).bind("keydown.dialog-overlay",function(r){var i=e.ui.dialog.overlay.instances;i.length!==0&&i[i.length-1]===n&&t.options.closeOnEscape&&!r.isDefaultPrevented()&&r.keyCode&&r.keyCode===e.ui.keyCode.ESCAPE&&(t.close(r),r.preventDefault())}),n.appendTo(document.body).css({width:this.width(),height:this.height()}),e.fn.bgiframe&&n.bgiframe(),this
.instances.push(n),n},destroy:function(t){var n=e.inArray(t,this.instances),r=0;n!==-1&&this.oldInstances.push(this.instances.splice(n,1)[0]),this.instances.length===0&&e([document,window]).unbind(".dialog-overlay"),t.height(0).width(0).remove(),e.each(this.instances,function(){r=Math.max(r,this.css("z-index"))}),this.maxZ=r},height:function(){var t,n;return e.ui.ie?(t=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight),n=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight),t<n?e(window).height()+"px":t+"px"):e(document).height()+"px"},width:function(){var t,n;return e.ui.ie?(t=Math.max(document.documentElement.scrollWidth,document.body.scrollWidth),n=Math.max(document.documentElement.offsetWidth,document.body.offsetWidth),t<n?e(window).width()+"px":t+"px"):e(document).width()+"px"},resize:function(){var t=e([]);e.each(e.ui.dialog.overlay.instances,function(){t=t.add(this)}),t.css({width:0,height:0}).css({width:e.ui.dialog.overlay
.width(),height:e.ui.dialog.overlay.height()})}}),e.extend(e.ui.dialog.overlay.prototype,{destroy:function(){e.ui.dialog.overlay.destroy(this.$el)}})})(jQuery);(function(e,t){var n=!1;e.widget("ui.menu",{version:"1.9.2",defaultElement:"<ul>",delay:300,options:{icons:{submenu:"ui-icon-carat-1-e"},menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.element.uniqueId().addClass("ui-menu ui-widget ui-widget-content ui-corner-all").toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length).attr({role:this.options.role,tabIndex:0}).bind("click"+this.eventNamespace,e.proxy(function(e){this.options.disabled&&e.preventDefault()},this)),this.options.disabled&&this.element.addClass("ui-state-disabled").attr("aria-disabled","true"),this._on({"mousedown .ui-menu-item > a":function(e){e.preventDefault()},"click .ui-state-disabled > a":function(e){e.preventDefault()},"click .ui-menu-it
em:has(a)":function(t){var r=e(t.target).closest(".ui-menu-item");!n&&r.not(".ui-state-disabled").length&&(n=!0,this.select(t),r.has(".ui-menu").length?this.expand(t):this.element.is(":focus")||(this.element.trigger("focus",[!0]),this.active&&this.active.parents(".ui-menu").length===1&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(t){var n=e(t.currentTarget);n.siblings().children(".ui-state-active").removeClass("ui-state-active"),this.focus(t,n)},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(e,t){var n=this.active||this.element.children(".ui-menu-item").eq(0);t||this.focus(e,n)},blur:function(t){this._delay(function(){e.contains(this.element[0],this.document[0].activeElement)||this.collapseAll(t)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){e(t.target).closest(".ui-menu").length||this.collapseAll(t),n=!1}})},_destroy:function(){this.element.removeAttr("aria-activedescendant").find(".ui-menu")
.andSelf().removeClass("ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons").removeAttr("role").removeAttr("tabIndex").removeAttr("aria-labelledby").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-disabled").removeUniqueId().show(),this.element.find(".ui-menu-item").removeClass("ui-menu-item").removeAttr("role").removeAttr("aria-disabled").children("a").removeUniqueId().removeClass("ui-corner-all ui-state-hover").removeAttr("tabIndex").removeAttr("role").removeAttr("aria-haspopup").children().each(function(){var t=e(this);t.data("ui-menu-submenu-carat")&&t.remove()}),this.element.find(".ui-menu-divider").removeClass("ui-menu-divider ui-widget-content")},_keydown:function(t){function a(e){return e.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}var n,r,i,s,o,u=!0;switch(t.keyCode){case e.ui.keyCode.PAGE_UP:this.previousPage(t);break;case e.ui.keyCode.PAGE_DOWN:this.nextPage(t);break;case e.ui.keyCode.HOME:this._move("first","first",t);break;c
ase e.ui.keyCode.END:this._move("last","last",t);break;case e.ui.keyCode.UP:this.previous(t);break;case e.ui.keyCode.DOWN:this.next(t);break;case e.ui.keyCode.LEFT:this.collapse(t);break;case e.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(t);break;case e.ui.keyCode.ENTER:case e.ui.keyCode.SPACE:this._activate(t);break;case e.ui.keyCode.ESCAPE:this.collapse(t);break;default:u=!1,r=this.previousFilter||"",i=String.fromCharCode(t.keyCode),s=!1,clearTimeout(this.filterTimer),i===r?s=!0:i=r+i,o=new RegExp("^"+a(i),"i"),n=this.activeMenu.children(".ui-menu-item").filter(function(){return o.test(e(this).children("a").text())}),n=s&&n.index(this.active.next())!==-1?this.active.nextAll(".ui-menu-item"):n,n.length||(i=String.fromCharCode(t.keyCode),o=new RegExp("^"+a(i),"i"),n=this.activeMenu.children(".ui-menu-item").filter(function(){return o.test(e(this).children("a").text())})),n.length?(this.focus(t,n),n.length>1?(this.previousFilter=i,this.fil
terTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter):delete this.previousFilter}u&&t.preventDefault()},_activate:function(e){this.active.is(".ui-state-disabled")||(this.active.children("a[aria-haspopup='true']").length?this.expand(e):this.select(e))},refresh:function(){var t,n=this.options.icons.submenu,r=this.element.find(this.options.menus);r.filter(":not(.ui-menu)").addClass("ui-menu ui-widget ui-widget-content ui-corner-all").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var t=e(this),r=t.prev("a"),i=e("<span>").addClass("ui-menu-icon ui-icon "+n).data("ui-menu-submenu-carat",!0);r.attr("aria-haspopup","true").prepend(i),t.attr("aria-labelledby",r.attr("id"))}),t=r.add(this.element),t.children(":not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","presentation").children("a").uniqueId().addClass("ui-corner-all").attr({tabIndex:-1,role:this._itemRole()}),t.children("
:not(.ui-menu-item)").each(function(){var t=e(this);/[^\-—–\s]/.test(t.text())||t.addClass("ui-widget-content ui-menu-divider")}),t.children(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!e.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},focus:function(e,t){var n,r;this.blur(e,e&&e.type==="focus"),this._scrollIntoView(t),this.active=t.first(),r=this.active.children("a").addClass("ui-state-focus"),this.options.role&&this.element.attr("aria-activedescendant",r.attr("id")),this.active.parent().closest(".ui-menu-item").children("a:first").addClass("ui-state-active"),e&&e.type==="keydown"?this._close():this.timer=this._delay(function(){this._close()},this.delay),n=t.children(".ui-menu"),n.length&&/^mouse/.test(e.type)&&this._startOpening(n),this.activeMenu=t.parent(),this._trigger("focus",e,{item:t})},_scrollIntoView:function(t){var n,r,i,s,o,u;this._hasScroll()&&(n=pars
eFloat(e.css(this.activeMenu[0],"borderTopWidth"))||0,r=parseFloat(e.css(this.activeMenu[0],"paddingTop"))||0,i=t.offset().top-this.activeMenu.offset().top-n-r,s=this.activeMenu.scrollTop(),o=this.activeMenu.height(),u=t.height(),i<0?this.activeMenu.scrollTop(s+i):i+u>o&&this.activeMenu.scrollTop(s+i-o+u))},blur:function(e,t){t||clearTimeout(this.timer);if(!this.active)return;this.active.children("a").removeClass("ui-state-focus"),this.active=null,this._trigger("blur",e,{item:this.active})},_startOpening:function(e){clearTimeout(this.timer);if(e.attr("aria-hidden")!=="true")return;this.timer=this._delay(function(){this._close(),this._open(e)},this.delay)},_open:function(t){var n=e.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(t.parents(".ui-menu")).hide().attr("aria-hidden","true"),t.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(n)},collapseAll:function(t,n){clearTimeout(this.timer),this.tim
er=this._delay(function(){var r=n?this.element:e(t&&t.target).closest(this.element.find(".ui-menu"));r.length||(r=this.element),this._close(r),this.blur(t),this.activeMenu=r},this.delay)},_close:function(e){e||(e=this.active?this.active.parent():this.element),e.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false").end().find("a.ui-state-active").removeClass("ui-state-active")},collapse:function(e){var t=this.active&&this.active.parent().closest(".ui-menu-item",this.element);t&&t.length&&(this._close(),this.focus(e,t))},expand:function(e){var t=this.active&&this.active.children(".ui-menu ").children(".ui-menu-item").first();t&&t.length&&(this._open(t.parent()),this._delay(function(){this.focus(e,t)}))},next:function(e){this._move("next","first",e)},previous:function(e){this._move("prev","last",e)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".
ui-menu-item").length},_move:function(e,t,n){var r;this.active&&(e==="first"||e==="last"?r=this.active[e==="first"?"prevAll":"nextAll"](".ui-menu-item").eq(-1):r=this.active[e+"All"](".ui-menu-item").eq(0));if(!r||!r.length||!this.active)r=this.activeMenu.children(".ui-menu-item")[t]();this.focus(n,r)},nextPage:function(t){var n,r,i;if(!this.active){this.next(t);return}if(this.isLastItem())return;this._hasScroll()?(r=this.active.offset().top,i=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return n=e(this),n.offset().top-r-i<0}),this.focus(t,n)):this.focus(t,this.activeMenu.children(".ui-menu-item")[this.active?"last":"first"]())},previousPage:function(t){var n,r,i;if(!this.active){this.next(t);return}if(this.isFirstItem())return;this._hasScroll()?(r=this.active.offset().top,i=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return n=e(this),n.offset().top-r+i>0}),this.focus(t,n)):this.focus(t,this.activeMenu.children(".u
i-menu-item").first())},_hasScroll:function(){return this.element.outerHeight()<this.element.prop("scrollHeight")},select:function(t){this.active=this.active||e(t.target).closest(".ui-menu-item");var n={item:this.active};this.active.has(".ui-menu").length||this.collapseAll(t,!0),this._trigger("select",t,n)}})})(jQuery);(function(e){function t(e){return function(){var t=this.element.val();e.apply(this,arguments),this._refresh(),t!==this.element.val()&&this._trigger("change")}}e.widget("ui.spinner",{version:"1.9.2",defaultElement:"<input>",widgetEventPrefix:"spin",options:{culture:null,icons:{down:"ui-icon-triangle-1-s",up:"ui-icon-triangle-1-n"},incremental:!0,max:null,min:null,numberFormat:null,page:10,step:1,change:null,spin:null,start:null,stop:null},_create:function(){this._setOption("max",this.options.max),this._setOption("min",this.options.min),this._setOption("step",this.options.step),this._value(this.element.val(),!0),this._draw(),this._on(this._events),this._refresh(
),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_getCreateOptions:function(){var t={},n=this.element;return e.each(["min","max","step"],function(e,r){var i=n.attr(r);i!==undefined&&i.length&&(t[r]=i)}),t},_events:{keydown:function(e){this._start(e)&&this._keydown(e)&&e.preventDefault()},keyup:"_stop",focus:function(){this.previous=this.element.val()},blur:function(e){if(this.cancelBlur){delete this.cancelBlur;return}this._refresh(),this.previous!==this.element.val()&&this._trigger("change",e)},mousewheel:function(e,t){if(!t)return;if(!this.spinning&&!this._start(e))return!1;this._spin((t>0?1:-1)*this.options.step,e),clearTimeout(this.mousewheelTimer),this.mousewheelTimer=this._delay(function(){this.spinning&&this._stop(e)},100),e.preventDefault()},"mousedown .ui-spinner-button":function(t){function r(){var e=this.element[0]===this.document[0].activeElement;e||(this.element.focus(),this.previous=n,this._delay(function(){this.previou
s=n}))}var n;n=this.element[0]===this.document[0].activeElement?this.previous:this.element.val(),t.preventDefault(),r.call(this),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur,r.call(this)});if(this._start(t)===!1)return;this._repeat(null,e(t.currentTarget).hasClass("ui-spinner-up")?1:-1,t)},"mouseup .ui-spinner-button":"_stop","mouseenter .ui-spinner-button":function(t){if(!e(t.currentTarget).hasClass("ui-state-active"))return;if(this._start(t)===!1)return!1;this._repeat(null,e(t.currentTarget).hasClass("ui-spinner-up")?1:-1,t)},"mouseleave .ui-spinner-button":"_stop"},_draw:function(){var e=this.uiSpinner=this.element.addClass("ui-spinner-input").attr("autocomplete","off").wrap(this._uiSpinnerHtml()).parent().append(this._buttonHtml());this.element.attr("role","spinbutton"),this.buttons=e.find(".ui-spinner-button").attr("tabIndex",-1).button().removeClass("ui-corner-all"),this.buttons.height()>Math.ceil(e.height()*.5)&&e.height()>0&&e.height(e.height()),t
his.options.disabled&&this.disable()},_keydown:function(t){var n=this.options,r=e.ui.keyCode;switch(t.keyCode){case r.UP:return this._repeat(null,1,t),!0;case r.DOWN:return this._repeat(null,-1,t),!0;case r.PAGE_UP:return this._repeat(null,n.page,t),!0;case r.PAGE_DOWN:return this._repeat(null,-n.page,t),!0}return!1},_uiSpinnerHtml:function(){return"<span class='ui-spinner ui-widget ui-widget-content ui-corner-all'></span>"},_buttonHtml:function(){return"<a class='ui-spinner-button ui-spinner-up ui-corner-tr'><span class='ui-icon "+this.options.icons.up+"'>▲</span>"+"</a>"+"<a class='ui-spinner-button ui-spinner-down ui-corner-br'>"+"<span class='ui-icon "+this.options.icons.down+"'>▼</span>"+"</a>"},_start:function(e){return!this.spinning&&this._trigger("start",e)===!1?!1:(this.counter||(this.counter=1),this.spinning=!0,!0)},_repeat:function(e,t,n){e=e||500,clearTimeout(this.timer),this.timer=this._delay(function(){this._repeat(40,t,n)},e),this._spin(t*this.opti
ons.step,n)},_spin:function(e,t){var n=this.value()||0;this.counter||(this.counter=1),n=this._adjustValue(n+e*this._increment(this.counter));if(!this.spinning||this._trigger("spin",t,{value:n})!==!1)this._value(n),this.counter++},_increment:function(t){var n=this.options.incremental;return n?e.isFunction(n)?n(t):Math.floor(t*t*t/5e4-t*t/500+17*t/200+1):1},_precision:function(){var e=this._precisionOf(this.options.step);return this.options.min!==null&&(e=Math.max(e,this._precisionOf(this.options.min))),e},_precisionOf:function(e){var t=e.toString(),n=t.indexOf(".");return n===-1?0:t.length-n-1},_adjustValue:function(e){var t,n,r=this.options;return t=r.min!==null?r.min:0,n=e-t,n=Math.round(n/r.step)*r.step,e=t+n,e=parseFloat(e.toFixed(this._precision())),r.max!==null&&e>r.max?r.max:r.min!==null&&e<r.min?r.min:e},_stop:function(e){if(!this.spinning)return;clearTimeout(this.timer),clearTimeout(this.mousewheelTimer),this.counter=0,this.spinning=!1,this._trigger("stop",e)},_setOp
tion:function(e,t){if(e==="culture"||e==="numberFormat"){var n=this._parse(this.element.val());this.options[e]=t,this.element.val(this._format(n));return}(e==="max"||e==="min"||e==="step")&&typeof t=="string"&&(t=this._parse(t)),this._super(e,t),e==="disabled"&&(t?(this.element.prop("disabled",!0),this.buttons.button("disable")):(this.element.prop("disabled",!1),this.buttons.button("enable")))},_setOptions:t(function(e){this._super(e),this._value(this.element.val())}),_parse:function(e){return typeof e=="string"&&e!==""&&(e=window.Globalize&&this.options.numberFormat?Globalize.parseFloat(e,10,this.options.culture):+e),e===""||isNaN(e)?null:e},_format:function(e){return e===""?"":window.Globalize&&this.options.numberFormat?Globalize.format(e,this.options.numberFormat,this.options.culture):e},_refresh:function(){this.element.attr({"aria-valuemin":this.options.min,"aria-valuemax":this.options.max,"aria-valuenow":this._parse(this.element.val())})},_value:function(e,t){var n;e!==
""&&(n=this._parse(e),n!==null&&(t||(n=this._adjustValue(n)),e=this._format(n))),this.element.val(e),this._refresh()},_destroy:function(){this.element.removeClass("ui-spinner-input").prop("disabled",!1).removeAttr("autocomplete").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.uiSpinner.replaceWith(this.element)},stepUp:t(function(e){this._stepUp(e)}),_stepUp:function(e){this._spin((e||1)*this.options.step)},stepDown:t(function(e){this._stepDown(e)}),_stepDown:function(e){this._spin((e||1)*-this.options.step)},pageUp:t(function(e){this._stepUp((e||1)*this.options.page)}),pageDown:t(function(e){this._stepDown((e||1)*this.options.page)}),value:function(e){if(!arguments.length)return this._parse(this.element.val());t(this._value).call(this,e)},widget:function(){return this.uiSpinner}})})(jQuery);
\ No newline at end of file
commit b921882b2beadf67b9d2898e063eb48bd0748843
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:55:06 2014 +0100
Add the images used by jQuery
diff --git a/fedora_elections/static/images/ui-bg_flat_0_aaaaaa_40x100.png b/fedora_elections/static/images/ui-bg_flat_0_aaaaaa_40x100.png
new file mode 100644
index 0000000..5b5dab2
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_flat_0_aaaaaa_40x100.png differ
diff --git a/fedora_elections/static/images/ui-bg_flat_0_eeeeee_40x100.png b/fedora_elections/static/images/ui-bg_flat_0_eeeeee_40x100.png
new file mode 100644
index 0000000..e44f861
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_flat_0_eeeeee_40x100.png differ
diff --git a/fedora_elections/static/images/ui-bg_flat_55_ffffff_40x100.png b/fedora_elections/static/images/ui-bg_flat_55_ffffff_40x100.png
new file mode 100644
index 0000000..ac8b229
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_flat_55_ffffff_40x100.png differ
diff --git a/fedora_elections/static/images/ui-bg_flat_75_ffffff_40x100.png b/fedora_elections/static/images/ui-bg_flat_75_ffffff_40x100.png
new file mode 100644
index 0000000..ac8b229
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_flat_75_ffffff_40x100.png differ
diff --git a/fedora_elections/static/images/ui-bg_glass_65_ffffff_1x400.png b/fedora_elections/static/images/ui-bg_glass_65_ffffff_1x400.png
new file mode 100644
index 0000000..42ccba2
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_glass_65_ffffff_1x400.png differ
diff --git a/fedora_elections/static/images/ui-bg_highlight-soft_100_f6f6f6_1x100.png b/fedora_elections/static/images/ui-bg_highlight-soft_100_f6f6f6_1x100.png
new file mode 100644
index 0000000..5dcfaa9
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_highlight-soft_100_f6f6f6_1x100.png differ
diff --git a/fedora_elections/static/images/ui-bg_highlight-soft_25_3c6eb4_1x100.png b/fedora_elections/static/images/ui-bg_highlight-soft_25_3c6eb4_1x100.png
new file mode 100644
index 0000000..a9ffc07
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_highlight-soft_25_3c6eb4_1x100.png differ
diff --git a/fedora_elections/static/images/ui-bg_highlight-soft_50_dddddd_1x100.png b/fedora_elections/static/images/ui-bg_highlight-soft_50_dddddd_1x100.png
new file mode 100644
index 0000000..b47a4da
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_highlight-soft_50_dddddd_1x100.png differ
diff --git a/fedora_elections/static/images/ui-bg_highlight-soft_75_dddddd_1x100.png b/fedora_elections/static/images/ui-bg_highlight-soft_75_dddddd_1x100.png
new file mode 100644
index 0000000..8392ac7
Binary files /dev/null and b/fedora_elections/static/images/ui-bg_highlight-soft_75_dddddd_1x100.png differ
diff --git a/fedora_elections/static/images/ui-icons_3c6eb4_256x240.png b/fedora_elections/static/images/ui-icons_3c6eb4_256x240.png
new file mode 100644
index 0000000..ecf79bc
Binary files /dev/null and b/fedora_elections/static/images/ui-icons_3c6eb4_256x240.png differ
diff --git a/fedora_elections/static/images/ui-icons_454545_256x240.png b/fedora_elections/static/images/ui-icons_454545_256x240.png
new file mode 100644
index 0000000..59bd45b
Binary files /dev/null and b/fedora_elections/static/images/ui-icons_454545_256x240.png differ
diff --git a/fedora_elections/static/images/ui-icons_666666_256x240.png b/fedora_elections/static/images/ui-icons_666666_256x240.png
new file mode 100644
index 0000000..f87de1c
Binary files /dev/null and b/fedora_elections/static/images/ui-icons_666666_256x240.png differ
diff --git a/fedora_elections/static/images/ui-icons_db3279_256x240.png b/fedora_elections/static/images/ui-icons_db3279_256x240.png
new file mode 100644
index 0000000..67b5713
Binary files /dev/null and b/fedora_elections/static/images/ui-icons_db3279_256x240.png differ
diff --git a/fedora_elections/static/images/ui-icons_ffffff_256x240.png b/fedora_elections/static/images/ui-icons_ffffff_256x240.png
new file mode 100644
index 0000000..42f8f99
Binary files /dev/null and b/fedora_elections/static/images/ui-icons_ffffff_256x240.png differ
commit 8a7fe9955abc63cea5f7966e39386a76010fcde9
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:54:51 2014 +0100
Add the election logo
diff --git a/fedora_elections/static/elections-logo.png b/fedora_elections/static/elections-logo.png
new file mode 100644
index 0000000..9ef1ec3
Binary files /dev/null and b/fedora_elections/static/elections-logo.png differ
commit fbbc1340bab17f09a0fd47db6b727cae29b2e846
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:54:43 2014 +0100
If there is no elections, keep going this will be our admin index page
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 5747469..4907395 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -441,10 +441,6 @@ def admin_view_elections():
elections = models.Election.search(
SESSION, fas_user=flask.g.fas_user.username)
- if not elections:
- flask.flash('You do not have any elections.')
- return redirect.safe_redirect_back()
-
return flask.render_template(
'admin/all_elections.html',
elections=elections)
commit 6eee058b8f7b68fc709a4b30f64a9e04ed74f8b4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:29:24 2014 +0100
Inject some variables into every templates
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 1905d8b..5747469 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -131,6 +131,17 @@ def get_valid_election(election_alias, ended=False):
return election
+(a)APP.context_processor
+def inject_variables():
+ ''' Inject a set of variable that we want for every pages (every
+ template).
+ '''
+ user = None
+ if hasattr(flask.g, 'fas_user'):
+ user = flask.g.fas_user
+ return dict(is_admin=is_elections_admin(user),
+ version=__version__)
+
### LIST VIEWS #############################################
commit 9300114324a2d2900024141a35702e8c94d0f180
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:29:02 2014 +0100
Add a top level __version__ and use this in the setup.py
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 55d4725..1905d8b 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -24,6 +24,8 @@
# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
+__version__ = '0.2.0'
+
import flask
from flask.ext.fas_openid import FAS
diff --git a/setup.py b/setup.py
index e71e5b9..b05b39b 100644
--- a/setup.py
+++ b/setup.py
@@ -2,15 +2,17 @@
__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
import pkg_resources
+from fedora_elections import __version__
from setuptools import setup, find_packages
-setup( name = 'fedora-elections'
- , version = '0.2'
- , author = 'Frank Chiulli'
- , author_email = 'fchiulli(a)fedoraproject.org'
- , packages = find_packages()
- , zip_safe = False
- , include_package_data = True
- , install_requires = ['Flask', 'SQLAlchemy>=0.7', 'python-fedora']
- )
+setup(
+ name = 'fedora-elections',
+ version = __version__,
+ author = 'Frank Chiulli',
+ author_email = 'fchiulli(a)fedoraproject.org',
+ packages = find_packages(),
+ zip_safe = False,
+ include_package_data = True,
+ install_requires = ['Flask', 'SQLAlchemy>=0.7', 'python-fedora'],
+)
commit 54d9636866de98e8b95cc1fc3854e6b2afe4c488
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:26:35 2014 +0100
Create a dedicated function to check is the user is an admin
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 8ac3eb8..55d4725 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -63,9 +63,21 @@ from fedora_elections import forms
from fedora_elections import redirect
-def remove_csrf(form_data):
- return dict([(k, v) for k, v in form_data.items() if k != 'csrf'])
+def is_elections_admin(user):
+ ''' Is the user an elections admin.
+ '''
+ if not user:
+ return False
+ if not user.cla_done or len(user.groups) < 1:
+ return False
+
+ admins = APP.config['FEDORA_ELECTIONS_ADMIN_GROUP']
+ if isinstance(admins, basestring): # pragma: no cover
+ admins = set([admins])
+ else:
+ admins = set(admins)
+ return len(set(user.groups).intersection(admins)) > 0
def login_required(f):
@wraps(f)
@@ -83,8 +95,7 @@ def election_admin_required(f):
if not hasattr(flask.g, 'fas_user') or flask.g.fas_user is None:
return flask.redirect(flask.url_for(
'auth_login', next=flask.request.url))
- if APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
- flask.g.fas_user.groups:
+ if not is_elections_admin(flask.g.fas_user):
flask.abort(403)
return f(*args, **kwargs)
return decorated_function
commit bb4d8b11b59a505aa9c98e67458bba46f73efdbd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:20:39 2014 +0100
Fix link, we have auth_login not login here
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 8f61812..8ac3eb8 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -392,7 +392,7 @@ def auth_login():
if 'next' in flask.request.args:
next_url = flask.request.args['next']
- if not next_url or next_url == flask.url_for('.login'):
+ if not next_url or next_url == flask.url_for('.auth_login'):
next_url = flask.url_for('.index')
if hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None:
commit 205bab3fb34afe8722b02ccae54214371ca693f2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:16:53 2014 +0100
Add some missing dependencies
diff --git a/requirements.txt b/requirements.txt
index 3887d38..ca5b050 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,4 +4,6 @@ flask
kitchen
python-fedora
Flask-wtf
-
+python-openid
+python-openid-cla
+python-openid-teams
commit 5bc2cf82f27297ed34cbd892686ee8a9603188f8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:58:51 2014 +0100
Remove the login template, no longer used
diff --git a/fedora_elections/templates/auth/login.html b/fedora_elections/templates/auth/login.html
deleted file mode 100644
index 9e7b73c..0000000
--- a/fedora_elections/templates/auth/login.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}Log in{% endblock %}
-{% block body %}
- <h2>Log in</h2>
-
- <form action="" method="post">
- <p>
- Log in using your
- <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a>
- account.
- </p>
- {{ render_field(form.username) }}
- {{ render_field(form.password) }}
- <p class="buttons indent"><input type="submit" class="submit positive"
- value="Log in">
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
commit b2ec7df1a113ab1b64a54320847055779074344d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:58:15 2014 +0100
Little clean up in the master template
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
index 3e6f806..47ab8bc 100644
--- a/fedora_elections/templates/base.html
+++ b/fedora_elections/templates/base.html
@@ -48,24 +48,24 @@
{% if g.fas_user %}
<li>
Hello, {{ g.fas_user.username }} —
- <a href="{{ url_for("auth_logout") }}">log out</a>
+ <a href="{{ url_for('auth_logout') }}">log out</a>
</li>
{% else %}
<li>
You are not logged in —
- <a href="{{ url_for("auth_login") }}">log in</a>
+ <a href="{{ url_for('auth_login') }}">log in</a>
</li>
{% endif %}
</ul>
<h2>Elections</h2>
<ul>
<li>
- <a href="{{ url_for("open_elections") }}">
+ <a href="{{ url_for('open_elections') }}">
Open elections
</a>
</li>
<li>
- <a href="{{ url_for("archived_elections") }}">
+ <a href="{{ url_for('archived_elections') }}">
Archived elections
</a>
</li>
@@ -74,12 +74,12 @@
<h2>Administration</h2>
<ul>
<li>
- <a href="{{ url_for("admin_view_elections") }}">
+ <a href="{{ url_for('admin_view_elections') }}">
My elections
</a>
</li>
<li>
- <a href="{{ url_for("admin_new_election") }}">
+ <a href="{{ url_for('admin_new_election') }}">
Create new election
</a>
</li>
@@ -101,7 +101,7 @@
<div id="footer">
{% block footer %}
<p class="copy">
- Copyright © 2008 Red Hat, Inc. and others. Please send any
+ Copyright © 2014 Red Hat, Inc. and others. Please send any
comments or corrections to the
<a href="mailto:webmaster@fedoraproject.org">
websites team
commit b00ae49e4e8d8eec971822d6078fe34e61e77d0b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:58:05 2014 +0100
Remove the login form, no longer needed
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 63a38c6..26d602c 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -90,12 +90,5 @@ class CandidateForm(wtf.Form):
url = wtforms.TextField('URL', [wtforms.validators.Length(max=250)])
-class LoginForm(wtf.Form):
- username = wtforms.TextField(
- 'Username', [wtforms.validators.Required()])
- password = wtforms.PasswordField(
- 'Password', [wtforms.validators.Required()])
-
-
class ConfirmationForm(wtf.Form):
pass
commit f6b0d247ddb6071b87f6d627fd48e430e7fcfc17
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:57:55 2014 +0100
Adjust the requirements to list python-fedora and not flask-fas
diff --git a/requirements.txt b/requirements.txt
index 336fbab..3887d38 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,6 +3,5 @@
flask
kitchen
python-fedora
-Flask-FAS
Flask-wtf
diff --git a/setup.py b/setup.py
index b05d9ba..e71e5b9 100644
--- a/setup.py
+++ b/setup.py
@@ -12,5 +12,5 @@ setup( name = 'fedora-elections'
, packages = find_packages()
, zip_safe = False
, include_package_data = True
- , install_requires = ['Flask', 'SQLAlchemy>=0.7']
+ , install_requires = ['Flask', 'SQLAlchemy>=0.7', 'python-fedora']
)
commit 07e088331296bd7b53bbcd595beb1b69a36e12b4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:57:18 2014 +0100
Port from flask-fas to flask-fas-openid
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index a073eae..8f61812 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -25,7 +25,7 @@
#
import flask
-from flask.ext.fas import FAS
+from flask.ext.fas_openid import FAS
from fedora.client import AuthError, AppError
from fedora.client.fas2 import AccountSystem
@@ -70,9 +70,9 @@ def remove_csrf(form_data):
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
- if flask.g.fas_user is None:
- return flask.redirect(flask.url_for('auth_login',
- next=flask.request.url))
+ if not hasattr(flask.g, 'fas_user') or flask.g.fas_user is None:
+ return flask.redirect(flask.url_for(
+ 'auth_login', next=flask.request.url))
return f(*args, **kwargs)
return decorated_function
@@ -80,9 +80,9 @@ def login_required(f):
def election_admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
- if flask.g.fas_user is None:
- return flask.redirect(flask.url_for('auth_login',
- next=flask.request.url))
+ if not hasattr(flask.g, 'fas_user') or flask.g.fas_user is None:
+ return flask.redirect(flask.url_for(
+ 'auth_login', next=flask.request.url))
if APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
flask.g.fas_user.groups:
flask.abort(403)
@@ -388,24 +388,24 @@ def vote_simple(election_alias):
def auth_login():
if flask.g.fas_user:
return redirect.safe_redirect_back()
- form = forms.LoginForm()
- if form.validate_on_submit():
- if FAS.login(form.username.data, form.password.data):
- flask.flash('Welcome, %s' % flask.g.fas_user.username)
- return redirect.safe_redirect_back()
- else:
- flask.flash('Incorrect username or password')
- return flask.render_template(
- 'auth/login.html',
- form=form)
+ next_url = None
+ if 'next' in flask.request.args:
+ next_url = flask.request.args['next']
+
+ if not next_url or next_url == flask.url_for('.login'):
+ next_url = flask.url_for('.index')
+
+ if hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None:
+ return flask.redirect(next_url)
+ else:
+ return FAS.login(return_url=next_url)
@APP.route('/logout')
def auth_logout():
- if not flask.g.fas_user:
- return redirect.safe_redirect_back()
- fas.logout()
- flask.flash('You have been logged out')
+ if hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None:
+ FAS.logout()
+ flask.flash('You have been logged out')
return redirect.safe_redirect_back()
commit 1ae930d03cc433fc8de7ecc4030e6583093f3159
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sun Mar 9 20:53:59 2014 +0100
Style change
Let's be consistent throughout the application on how we write the render_template
arguments.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 97817ce..a073eae 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -374,10 +374,12 @@ def vote_simple(election_alias):
usernamemap[candidate.id] = candidate.name
return flask.render_template(
- 'election/vote_simple.html', election=election,
+ 'election/vote_simple.html',
+ election=election,
num_candidates=num_candidates,
usernamemap=usernamemap,
- candidate_id=candidate_id, nextaction=next_action)
+ candidate_id=candidate_id,
+ nextaction=next_action)
### AUTH VIEWS #############################################
@@ -393,7 +395,9 @@ def auth_login():
return redirect.safe_redirect_back()
else:
flask.flash('Incorrect username or password')
- return flask.render_template('auth/login.html', form=form)
+ return flask.render_template(
+ 'auth/login.html',
+ form=form)
@APP.route('/logout')
commit 0d9bd43f51c74646c3717e31b0769bb1ebba82ba
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat Mar 1 07:49:39 2014 +0100
pep8 fixes
diff --git a/fedmsg.d/base.py b/fedmsg.d/base.py
index dc1d155..581c207 100644
--- a/fedmsg.d/base.py
+++ b/fedmsg.d/base.py
@@ -32,8 +32,8 @@ config = dict(
# opposed to amqp or stomp.
zmq_enabled=True,
- # When subscribing to messages, we want to allow splats ('*') so we tell the
- # hub to not be strict when comparing messages topics to subscription
+ # When subscribing to messages, we want to allow splats ('*') so we tell
+ # the hub to not be strict when comparing messages topics to subscription
# topics.
zmq_strict=False,
)
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 5cf5e68..97817ce 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -238,10 +238,10 @@ def vote_range(election_alias):
uvotes = {}
for candidate in election.candidates:
uvotes[candidate.id] = ""
- next_action = 'vote'
+ next_action = 'vote'
if flask.request.method == 'POST':
- next_action = ''
+ next_action = ''
form_values = flask.request.form.values()
if 'Submit' in form_values:
for candidate in election.candidates:
@@ -310,6 +310,7 @@ def vote_range(election_alias):
voteinfo=uvotes,
nextaction=next_action)
+
@APP.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
@login_required
def vote_simple(election_alias):
@@ -330,11 +331,11 @@ def vote_simple(election_alias):
return redirect.safe_redirect_back()
num_candidates = election.candidates.count()
- next_action = 'vote'
+ next_action = 'vote'
candidate_id = -1
if flask.request.method == 'POST':
- next_action = ''
+ next_action = ''
form_values = flask.request.form.values()
if 'Submit' in form_values:
candidate_id = int(flask.request.form['candidate'])
@@ -372,10 +373,12 @@ def vote_simple(election_alias):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
- return flask.render_template('election/vote_simple.html', election=election,
- num_candidates=num_candidates,
- usernamemap=usernamemap,
- candidate_id=candidate_id, nextaction=next_action)
+ return flask.render_template(
+ 'election/vote_simple.html', election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ candidate_id=candidate_id, nextaction=next_action)
+
### AUTH VIEWS #############################################
@@ -437,7 +440,7 @@ def admin_new_election():
frontpage=form.frontpage.data,
voting_type=form.voting_type.data,
candidates_are_fasusers=form.candidates_are_fasusers.data,
- fas_user = flask.g.fas_user.username,
+ fas_user=flask.g.fas_user.username,
)
# Fix start_date and end_date to use datetime
@@ -625,4 +628,3 @@ def election_results(election_alias):
'election/results.html',
election=election,
usernamemap=usernamemap)
-
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
index 3afb452..bfa6d01 100644
--- a/fedora_elections/default_config.py
+++ b/fedora_elections/default_config.py
@@ -9,7 +9,7 @@ SECRET_KEY = 'change me'
FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
FAS_USERNAME = 'elections'
-FAS_PASSWORD ='el3ctMEn0w'
+FAS_PASSWORD = 'el3ctMEn0w'
FAS_CHECK_CERT = False
FAS_HTTPS_REQUIRED = False
diff --git a/fedora_elections/fedmsgshim.py b/fedora_elections/fedmsgshim.py
index 775f785..2e13398 100644
--- a/fedora_elections/fedmsgshim.py
+++ b/fedora_elections/fedmsgshim.py
@@ -1,6 +1,6 @@
""" This is a temporary shim that allows fedmsg to be an optional dependency of
-elections. If fedmsg is installed, these function calls will try to actually send
-messages. If it is not installed, it will return silently.
+elections. If fedmsg is installed, these function calls will try to actually
+send messages. If it is not installed, it will return silently.
:Author: Ralph Bean <rbean(a)redhat.com>
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 5b7d838..63a38c6 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -17,9 +17,9 @@ class ElectionForm(wtf.Form):
wtforms.validators.Length(max=100),
wtforms.validators.Regexp(
'[a-z0-9_-]+',
- message=('Alias may only contain lower case letters, numbers, '
- 'hyphens and underscores.')),
- ])
+ message=('Alias may only contain lower case letters, numbers, '
+ 'hyphens and underscores.')),
+ ])
description = wtforms.TextAreaField(
'Description', [
@@ -27,9 +27,9 @@ class ElectionForm(wtf.Form):
voting_type = wtforms.RadioField(
'Type',
- choices=[('range', 'Range Voting'),
- ('simple', 'Simple Voting')],
- default='range')
+ choices=[('range', 'Range Voting'),
+ ('simple', 'Simple Voting')],
+ default='range')
url = wtforms.TextField(
'URL', [
@@ -46,7 +46,7 @@ class ElectionForm(wtf.Form):
wtforms.validators.Required()])
number_elected = wtforms.IntegerField(
- 'Number elected',[
+ 'Number elected', [
wtforms.validators.Required(),
wtforms.validators.NumberRange(min=1)],
default=1)
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 1911eaa..113d814 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -74,9 +74,8 @@ def create_session(db_url, debug=False, pool_recycle=3600):
:return a Session that can be used to query the database.
"""
- engine = sa.create_engine(db_url,
- echo=debug,
- pool_recycle=pool_recycle)
+ engine = sa.create_engine(
+ db_url, echo=debug, pool_recycle=pool_recycle)
scopedsession = scoped_session(sessionmaker(bind=engine))
return scopedsession
@@ -188,7 +187,6 @@ class ElectionAdminGroup(BASE):
role_required = sa.Column(sa.Enum(u'user', u'sponsor', u'administrator'),
nullable=False)
-
@classmethod
def by_election_id(cls, session, election_id):
""" Return all the ElectionAdminGroup having a specific election_id.
@@ -200,7 +198,6 @@ class ElectionAdminGroup(BASE):
).all()
-
class Candidate(BASE):
__tablename__ = 'candidates'
@@ -209,8 +206,8 @@ class Candidate(BASE):
nullable=False)
election = relationship(
'Election', backref=backref('candidates', lazy='dynamic'))
- name = sa.Column(sa.Unicode(150), nullable=False) # FAS username if
- # candidates_are_fasusers
+ # FAS username if candidates_are_fasusers
+ name = sa.Column(sa.Unicode(150), nullable=False)
url = sa.Column(sa.Unicode(250))
@property
@@ -244,7 +241,7 @@ class Vote(BASE):
__table_args__ = (sa.UniqueConstraint('election_id', 'voter',
'candidate_id',
- name='eid_voter_cid'), {} )
+ name='eid_voter_cid'), {})
id = sa.Column(sa.Integer, primary_key=True)
election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
diff --git a/fedora_elections/redirect.py b/fedora_elections/redirect.py
index 5571abe..e7aa43e 100644
--- a/fedora_elections/redirect.py
+++ b/fedora_elections/redirect.py
@@ -7,7 +7,7 @@ def is_safe_url(target):
ref_url = urlparse(flask.request.host_url)
test_url = urlparse(urljoin(flask.request.host_url, target))
return test_url.scheme in ('http', 'https') and \
- ref_url.netloc == test_url.netloc
+ ref_url.netloc == test_url.netloc
def safe_redirect_back(next=None, fallback=('index', {})):
commit 3b53a1b93d1a99983285847786ef0ee7b477f8c1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Feb 28 10:04:14 2014 +0100
Reduce indentation
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index 20b0d7a..fafefee 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -1,26 +1,26 @@
{% extends "base.html" %}
{% block title %}Election administration{% endblock %}
{% block body %}
- <h2>Election administration</h2>
- <table>
+ <h2>Election administration</h2>
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ </tr>
+ {% for election in elections %}
<tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
+ <td>
+ <a href="{{ url_for('admin_view_election',
+ election_alias=election.alias) }}">
+ {{ election.summary }}
+ </a>
+ </td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
</tr>
- {% for election in elections %}
- <tr>
- <td>
- <a href="{{ url_for('admin_view_election',
- election_alias=election.alias) }}">
- {{ election.summary }}
- </a>
- </td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- </tr>
- {% endfor %}
- </table>
+ {% endfor %}
+ </table>
{%endblock %}
commit d8586b46f778d3a848e48400eb4c0bdf510cf14f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Feb 20 11:21:51 2014 +0100
Adjust the runserver script
diff --git a/runserver.py b/runserver.py
index 9c845ba..293b6d0 100644
--- a/runserver.py
+++ b/runserver.py
@@ -2,5 +2,5 @@ import __main__
__main__.__requires__ = ['SQLAlchemy >= 0.7']
import pkg_resources
-from fedora_elections import app
-app.run()
+from fedora_elections import APP
+APP.run()
commit 19ae57db972a8c0bd99b3a99b7baca7ad26c9e3b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:40:17 2014 +0100
Fix typo when retrieving a valid election
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 78285e3..5cf5e68 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -110,7 +110,7 @@ def get_valid_election(election_alias, ended=False):
return flask.redirect(flask.url_for(
'election_results', election_alias=election.alias))
- elif not ended and election.status == 'In progress':
+ elif ended and election.status == 'In progress':
flask.flash(
'Sorry but this election is in progress, you may not see its '
'results already.')
commit b655d21a06a76740e6a245da05d177db99395d3e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:12:53 2014 +0100
Fix retrieving a valid election that have ended
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index c350da7..78285e3 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -90,7 +90,7 @@ def election_admin_required(f):
return decorated_function
-def get_valid_election(election_alias):
+def get_valid_election(election_alias, ended=False):
""" Return the election if it is valid (no pending, not ended).
"""
election = models.Election.get(SESSION, alias=election_alias)
@@ -103,12 +103,19 @@ def get_valid_election(election_alias):
flask.flash('Voting has not yet started, sorry.')
return redirect.safe_redirect_back()
- elif election.status == 'Ended':
+ elif not ended and election.status == 'Ended':
flask.flash(
'This election is closed. You have been redirected to the '
'election results.')
return flask.redirect(flask.url_for(
'election_results', election_alias=election.alias))
+
+ elif not ended and election.status == 'In progress':
+ flask.flash(
+ 'Sorry but this election is in progress, you may not see its '
+ 'results already.')
+ return redirect.safe_redirect_back()
+
return election
@@ -574,23 +581,17 @@ def admin_delete_candidate(election_alias, candidate_id):
@APP.route('/results/<election_alias>')
def election_results(election_alias):
- election = get_valid_election(election_alias)
+ election = get_valid_election(election_alias, ended=True)
if not isinstance(election, models.Election):
return election
- if election.status == 'In progress':
- flask.flash(
- 'Sorry but this election is in progress, you may not see its '
- 'results already.')
- return redirect.safe_redirect_back()
-
elif election.embargoed == 1:
if not hasattr(flask.g, 'fas_user') or not flask.g.fas_user:
- flask.flash("We are sorry. The results for this election"
- "cannot be viewed because they are currently"
- " embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
+ flask.flash("We are sorry. The results for this election"
+ "cannot be viewed because they are currently"
+ " embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
else:
if APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
flask.g.fas_user.groups:
@@ -620,5 +621,8 @@ def election_results(election_alias):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
- return flask.render_template('election/results.html', election=election,
- usernamemap=usernamemap)
+ return flask.render_template(
+ 'election/results.html',
+ election=election,
+ usernamemap=usernamemap)
+
commit c1959e1a6e7014a4360a4958ee7db70c5d881942
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:59:05 2014 +0100
Bug fix in Vote.of_user_on_election() method to account for the count argument
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 48d34d6..1911eaa 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -273,4 +273,7 @@ class Vote(BASE):
cls.voter == user
)
- return query.all()
+ if count:
+ return query.count()
+ else:
+ return query.all()
commit defc5157a6c0ad03820ba4ae951ce6d1e059dbcb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:51:54 2014 +0100
Adjust requirements
diff --git a/requirements.txt b/requirements.txt
index 89a2349..336fbab 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,5 +4,5 @@ flask
kitchen
python-fedora
Flask-FAS
-Flask-sqlalchemy
Flask-wtf
+
diff --git a/setup.py b/setup.py
index c1ac01e..b05d9ba 100644
--- a/setup.py
+++ b/setup.py
@@ -12,5 +12,5 @@ setup( name = 'fedora-elections'
, packages = find_packages()
, zip_safe = False
, include_package_data = True
- , install_requires = ['Flask', 'SQLAlchemy>=0.7', 'flask-sqlalchemy']
+ , install_requires = ['Flask', 'SQLAlchemy>=0.7']
)
commit 2ea19c044a74c73fc521fb7730720e8023ebd8c5
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:51:47 2014 +0100
Add decorator when login is required
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index ea7203b..c350da7 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -183,6 +183,7 @@ def open_elections():
### ELECTION VIEWS #############################################
@APP.route('/vote/<election_alias>', methods=['GET', 'POST'])
+@login_required
def vote(election_alias):
election = get_valid_election(election_alias)
@@ -207,6 +208,7 @@ def vote(election_alias):
@APP.route('/vote_range/<election_alias>', methods=['GET', 'POST'])
+@login_required
def vote_range(election_alias):
election = get_valid_election(election_alias)
@@ -214,8 +216,8 @@ def vote_range(election_alias):
return election
if (election.voting_type == 'simple'):
- return flask.redirect(flask.url_for('vote_simple',
- election_alias=election_alias))
+ return flask.redirect(flask.url_for(
+ 'vote_simple', election_alias=election_alias))
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
@@ -302,6 +304,7 @@ def vote_range(election_alias):
nextaction=next_action)
@APP.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
+@login_required
def vote_simple(election_alias):
election = get_valid_election(election_alias)
@@ -309,8 +312,8 @@ def vote_simple(election_alias):
return election
if (election.voting_type == 'range'):
- return flask.redirect(flask.url_for('vote_range',
- election_alias=election_alias))
+ return flask.redirect(flask.url_for(
+ 'vote_range', election_alias=election_alias))
votes = models.Vote.of_user_on_election(
SESSION, flask.g.fas_user.username, election.id, count=True)
@@ -583,7 +586,7 @@ def election_results(election_alias):
return redirect.safe_redirect_back()
elif election.embargoed == 1:
- if flask.g.fas_user is None:
+ if not hasattr(flask.g, 'fas_user') or not flask.g.fas_user:
flask.flash("We are sorry. The results for this election"
"cannot be viewed because they are currently"
" embargoed pending formal announcement.")
commit 18ea8617ae2fd177d84ef8f87eeced5bcd74dfdf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:13:25 2014 +0100
Add script to easily create the database required to work on elections
diff --git a/createdb.py b/createdb.py
new file mode 100644
index 0000000..af15e2d
--- /dev/null
+++ b/createdb.py
@@ -0,0 +1,7 @@
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7']
+import pkg_resources
+
+from fedora_elections import APP
+from fedora_elections.models import create_tables
+create_tables(APP.config['DB_URL'], debug=True)
commit 742b4ad06fad990d938eb3baeacf9e177820ac4d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:12:59 2014 +0100
Adjust the tests to run on the current application
diff --git a/tests/base.py b/tests/base.py
index e12179b..c0e5c73 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -1,10 +1,13 @@
# -*- coding: utf-8 -*-
#
-# Copyright (C) 2012 Red Hat, Inc.
+# Copyright (C) 2012-2014 Red Hat, Inc.
#
# Author(s): Toshio Kuratomi <toshio(a)fedoraproject.org>
+import os
+
import fedora_elections
+from fedora_elections.models import create_tables
# Note: This is used in unittests that use nose's generator tests so we can't
# subclass unittest.TestCase
@@ -12,20 +15,45 @@ class BaseRequestTests(object):
'''Setup application for further testing of requests'''
def setUp(self):
- fedora_elections.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + '../tests/testing.db'
- fedora_elections.app.config['TESTING'] = True
- self.app = fedora_elections.app.test_client()
- fedora_elections.db.create_all()
+ cur_folder = os.path.dirname(os.path.realpath(__file__))
+ db_file = os.path.join(cur_folder, 'testing.sqlite')
+ print '* creating %s' % db_file
+ fedora_elections.APP.config['DB_URL'] = 'sqlite:///%s' % db_file
+
+ self.session = create_tables(fedora_elections.APP.config['DB_URL'])
+
+ fedora_elections.APP.config['TESTING'] = True
+ fedora_elections.APP.SESSION = self.session
+ self.app = fedora_elections.APP.test_client()
def tearDown(self):
- fedora_elections.db.session.remove()
- fedora_elections.db.drop_all()
+ cur_folder = os.path.dirname(os.path.realpath(__file__))
+ db_file = os.path.join(cur_folder, 'testing.sqlite')
+ print '* cleaning %s' % db_file
+ os.unlink(db_file)
+
url_subs = {'election_alias': None, 'candidate_id': None}
-anon_urls = ('/', '/archive', '/login', '/logout', '/vote/%(election_alias)s')
-authed_view_urls = ('/logout', '/vote/%(election_alias)s')
-admin_view_urls = ('/admin', '/admin/new', '/admin/%(election_alias)s')
-admin_modify_urls = ('/admin/new', '/admin/%(election_alias)s/edit',
+
+anon_urls = (
+ '/',
+ '/archive',
+ '/login',
+ '/logout',
+ '/vote/%(election_alias)s')
+
+authed_view_urls = (
+ '/logout',
+ '/vote/%(election_alias)s')
+
+admin_view_urls = (
+ '/admin',
+ '/admin/new',
+ '/admin/%(election_alias)s')
+
+admin_modify_urls = (
+ '/admin/new',
+ '/admin/%(election_alias)s/edit',
'/admin/%(election_alias)s/candidates/new',
'/admin/%(election_alias)s/candidates/%(candidate_id)d/edit',
'/admin/%(election_alias)s/candidates/%(candidate_id)d/delete')
diff --git a/tests/test_smoke.py b/tests/test_smoke.py
index 1d7b306..0fbb3d2 100644
--- a/tests/test_smoke.py
+++ b/tests/test_smoke.py
@@ -14,26 +14,37 @@ from nose import tools, with_setup
from base import BaseRequestTests, anon_urls, authed_view_urls, \
admin_view_urls, admin_modify_urls, url_subs
-from fedora_elections import db, models
-from fedora_elections import fas
+import fedora_elections
+import fedora_elections.models
class TestSmoke(BaseRequestTests):
'''Basic test that we don't get 500s'''
def setUp(self):
super(TestSmoke, self).setUp()
- fas.login = Mock(return_value=True)
+ fedora_elections.SESSION = self.session
+
+ fedora_elections.FAS.login = Mock(return_value=True)
# Add an election
- election = models.Election(summary='Test Election 1',
- alias='test1', description='lorem ipsum of course',
+ election = fedora_elections.models.Election(
+ summary='Test Election 1',
+ alias='test1',
+ description='lorem ipsum of course',
url='http://fedorahosted.org/elections',
start_date=datetime.datetime.utcnow(),
- end_date=datetime.datetime.utcnow()+datetime.timedelta(hours=1))
- db.session.add(election)
+ end_date=datetime.datetime.utcnow()+datetime.timedelta(hours=1),
+ fas_user='fchiulli',
+ )
+ self.session.add(election)
+
# Add candidate to election
- candidate = models.Candidate(election=election, name='Mr Testy', url='http://fedoraproject.org')
- db.session.add(candidate)
- db.session.commit()
+ candidate = fedora_elections.models.Candidate(
+ election=election,
+ name='Mr Testy',
+ url='http://fedoraproject.org')
+ self.session.add(candidate)
+
+ self.session.commit()
self.url_subs = copy(url_subs)
self.url_subs['election_alias'] = 'test1'
@@ -64,10 +75,13 @@ class TestSmoke(BaseRequestTests):
self.check_status_code(*args, **kwargs)
def test_all_url_subs(self):
- '''Testing that the testsuite is going to substitute all variables in the url.'''
+ '''Testing that the testsuite is going to substitute all variables
+ in the url
+ '''
for i in self.url_subs.values():
if i is None:
- raise Exception('Smoke test must be updated for new url variable present in base.py')
+ raise Exception('Smoke test must be updated for new url '
+ 'variable present in base.py')
def test_anon_view(self):
for url in anon_urls:
commit a4690735ca927226587220e64c5e971e9121bac7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:12:32 2014 +0100
We no longer have a development configuration file to use on the tests
diff --git a/testing b/testing
index 529e8ee..76f7c3e 100755
--- a/testing
+++ b/testing
@@ -1,2 +1,3 @@
#!/bin/sh
-FEDORA_ELECTIONS_CONFIG=../development.cfg nosetests $*
+nosetests $*
+
commit 8a46c6a1a7233e5f70c3496e05a2609fb8b277de
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:12:10 2014 +0100
Port the flask application away from flask-sqlalchemy
This allows us to have all the querying done in one place
and not tight coupling of the logic with this library.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index c6e5f21..ea7203b 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -21,11 +21,11 @@
# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
# Toshio Kuratomi <tkuratom(a)redhat.com>
# Frank Chiulli <fchiulli(a)fedoraproject.org>
+# Pierre-Yves Chibon <pingou(a)fedoraproject.org>
#
import flask
from flask.ext.fas import FAS
-from flask.ext.sqlalchemy import SQLAlchemy
from fedora.client import AuthError, AppError
from fedora.client.fas2 import AccountSystem
@@ -38,30 +38,29 @@ from datetime import datetime, time
from functools import wraps
from urlparse import urlparse, urljoin
-app = flask.Flask(__name__)
-app.config.from_object('fedora_elections.default_config')
-app.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
-
-# set up SQLAlchemy
-db = SQLAlchemy(app)
+APP = flask.Flask(__name__)
+APP.config.from_object('fedora_elections.default_config')
+if APP.config.get('FEDORA_ELECTIONS_CONFIG') \
+ and os.path.exists(APP.config['FEDORA_ELECTIONS_CONFIG']):
+ APP.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
# set up FAS
-fas = FAS(app)
+FAS = FAS(APP)
# FAS for usernames.
-# baseURL = "https://admin.fedoraproject.org/accounts"
-# fasuser = "elections"
-# faspw = "elections"
-# fas2_url = 'http://fakefas.fedoraproject.org/accounts/'
-fas2_url = app.config['FAS_BASE_URL']
-fas2_username = app.config['FAS_USERNAME']
-fas2_password = app.config['FAS_PASSWORD']
-fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password,
- insecure=True)
+FAS2 = AccountSystem(
+ APP.config['FAS_BASE_URL'],
+ username=APP.config['FAS_USERNAME'],
+ password=APP.config['FAS_PASSWORD'],
+ insecure=APP.config['FAS_CHECK_CERT']
+)
# modular imports
-from fedora_elections import forms, models, redirect
+from fedora_elections import models
+SESSION = models.create_session(APP.config['DB_URL'])
+from fedora_elections import forms
+from fedora_elections import redirect
def remove_csrf(form_data):
@@ -84,29 +83,53 @@ def election_admin_required(f):
if flask.g.fas_user is None:
return flask.redirect(flask.url_for('auth_login',
next=flask.request.url))
- if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
+ if APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
flask.g.fas_user.groups:
flask.abort(403)
return f(*args, **kwargs)
return decorated_function
+def get_valid_election(election_alias):
+ """ Return the election if it is valid (no pending, not ended).
+ """
+ election = models.Election.get(SESSION, alias=election_alias)
+
+ if not election:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'Pending':
+ flask.flash('Voting has not yet started, sorry.')
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Ended':
+ flask.flash(
+ 'This election is closed. You have been redirected to the '
+ 'election results.')
+ return flask.redirect(flask.url_for(
+ 'election_results', election_alias=election.alias))
+ return election
+
+
### LIST VIEWS #############################################
-(a)app.route('/')
+(a)APP.route('/')
def index():
- elections = models.Election.query.filter_by(frontpage=True).all()
+ elections = models.Election.search(SESSION, frontpage=True)
num_elections = len(elections)
- return flask.render_template('list/index.html', elections=elections,
- num_elections=num_elections,
- title="Elections")
+ return flask.render_template(
+ 'list/index.html',
+ elections=elections,
+ num_elections=num_elections,
+ title="Elections")
-(a)app.route('/about/<election_alias>')
+(a)APP.route('/about/<election_alias>')
def about_election(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
+ election = models.Election.get(SESSION, alias=election_alias)
+
+ if not election:
flask.flash('The election, %s, does not exist.' % election_alias)
return redirect.safe_redirect_back()
@@ -120,115 +143,84 @@ def about_election(election_alias):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
- return flask.render_template('election/about.html', election=election,
- usernamemap=usernamemap)
+ return flask.render_template(
+ 'election/about.html',
+ election=election,
+ usernamemap=usernamemap)
-(a)app.route('/archive')
+(a)APP.route('/archive')
def archived_elections():
now = datetime.utcnow()
- try:
- elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date<now, models.Election.id>0).all()
- except NoResultFound:
- flask.flash('There are no archived elections.')
- return redirect.safe_redirect_back()
+ elections = models.Election.get_older_election(SESSION, now)
- num_elections = len(elections)
- if (num_elections == 0):
- flask.flash('There are no open elections.')
+ if not elections:
+ flask.flash('There are no archived elections.')
return redirect.safe_redirect_back()
- return flask.render_template('election/archive.html',
- num_elections=num_elections,
- elections=elections)
+ return flask.render_template(
+ 'election/archive.html',
+ elections=elections)
-(a)app.route('/open')
+(a)APP.route('/open')
def open_elections():
now = datetime.utcnow()
- try:
- elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date>now,
- models.Election.id>0).all()
- except NoResultFound:
- flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
+ elections = models.Election.get_open_election(SESSION, now)
- num_elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date>now,
- models.Election.id>0).count()
- if (num_elections == 0):
+ if not elections:
flask.flash('There are no open elections.')
return redirect.safe_redirect_back()
- return flask.render_template('list/index.html', elections=elections,
- num_elections=num_elections,
- title='Open Elections')
+ return flask.render_template(
+ 'list/index.html',
+ elections=elections,
+ title='Open Elections')
### ELECTION VIEWS #############################################
-(a)app.route('/vote/<election_alias>', methods=['GET', 'POST'])
+(a)APP.route('/vote/<election_alias>', methods=['GET', 'POST'])
def vote(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
+ election = get_valid_election(election_alias)
- if election.status == 'Pending':
- flask.flash('Voting has not yet started, sorry.')
- return redirect.safe_redirect_back()
+ if not isinstance(election, models.Election):
+ return election
- elif election.status == 'Ended':
- flask.flash('This election is closed. You have been redirected to ' +
- 'the election results.')
- return flask.redirect(flask.url_for('election_results',
- election_alias=election.alias))
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, election.id, count=True)
- votes = models.Vote.query.filter_by(election_id=election.id,
- voter=flask.g.fas_user.username).count()
- if (votes != 0):
+ if votes > 0:
flask.flash('You have already voted in the election!')
return redirect.safe_redirect_back()
- if (election.voting_type == 'range'):
+ if election.voting_type == 'range':
return vote_range(election_alias)
- elif (election.voting_type == 'simple'):
+ elif election.voting_type == 'simple':
return vote_simple(election_alias)
else:
- flask.flash('Unknown election voting type: %s' % election.voting_type)
+ flask.flash(
+ 'Unknown election voting type: %s' % election.voting_type)
return redirect.safe_redirect_back()
-(a)app.route('/vote_range/<election_alias>', methods=['GET', 'POST'])
+(a)APP.route('/vote_range/<election_alias>', methods=['GET', 'POST'])
def vote_range(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
+ election = get_valid_election(election_alias)
- if election.status == 'Pending':
- flask.flash('Voting has not yet started, sorry.')
- return redirect.safe_redirect_back()
-
- elif election.status == 'Ended':
- flask.flash('This election is closed. You have been redirected to ' +
- 'the election results.')
- return flask.redirect(flask.url_for('election_results',
- election_alias=election.alias))
+ if not isinstance(election, models.Election):
+ return election
if (election.voting_type == 'simple'):
return flask.redirect(flask.url_for('vote_simple',
election_alias=election_alias))
- votes = models.Vote.query.filter_by(election_id=election.id,
- voter=flask.g.fas_user.username).count()
- if (votes != 0):
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, election.id, count=True)
+
+ if votes > 0:
flask.flash('You have already voted in the election!')
return redirect.safe_redirect_back()
@@ -257,13 +249,15 @@ def vote_range(election_alias):
return redirect.safe_redirect_back()
for uvote in uvotes:
- new_vote = models.Vote(election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=uvote,
- value=uvotes[uvote] )
- db.session.add(new_vote)
- db.session.commit()
+ new_vote = models.Vote(
+ election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=uvote,
+ value=uvotes[uvote]
+ )
+ SESSION.add(new_vote)
+ SESSION.commit()
flask.flash("Your vote has been recorded. Thank you!")
return redirect.safe_redirect_back()
@@ -299,35 +293,28 @@ def vote_range(election_alias):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
- return flask.render_template('election/vote_range.html', election=election,
- num_candidates=num_candidates,
- usernamemap=usernamemap,
- voteinfo=uvotes, nextaction=next_action)
+ return flask.render_template(
+ 'election/vote_range.html',
+ election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ voteinfo=uvotes,
+ nextaction=next_action)
-(a)app.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
+(a)APP.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
def vote_simple(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
+ election = get_valid_election(election_alias)
- if election.status == 'Pending':
- flask.flash('Voting has not yet started, sorry.')
- return redirect.safe_redirect_back()
-
- elif election.status == 'Ended':
- flask.flash('This election is closed. You have been redirected to ' +
- 'the election results.')
- return flask.redirect(flask.url_for('election_results',
- election_alias=election_alias))
+ if not isinstance(election, models.Election):
+ return election
if (election.voting_type == 'range'):
return flask.redirect(flask.url_for('vote_range',
election_alias=election_alias))
- votes = models.Vote.query.filter_by(election_id=election.id,
- voter=flask.g.fas_user.username).count()
+ votes = models.Vote.of_user_on_election(
+ SESSION, flask.g.fas_user.username, election.id, count=True)
+
if (votes != 0):
flask.flash('You have already voted in the election!')
return redirect.safe_redirect_back()
@@ -341,13 +328,15 @@ def vote_simple(election_alias):
form_values = flask.request.form.values()
if 'Submit' in form_values:
candidate_id = int(flask.request.form['candidate'])
- new_vote = models.Vote(election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=candidate_id,
- value=1 )
- db.session.add(new_vote)
- db.session.commit()
+ new_vote = models.Vote(
+ election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=candidate_id,
+ value=1
+ )
+ SESSION.add(new_vote)
+ SESSION.commit()
flask.flash("Your vote has been recorded. Thank you!")
return redirect.safe_redirect_back()
@@ -380,13 +369,13 @@ def vote_simple(election_alias):
### AUTH VIEWS #############################################
-(a)app.route('/login', methods=('GET', 'POST'))
+(a)APP.route('/login', methods=('GET', 'POST'))
def auth_login():
if flask.g.fas_user:
return redirect.safe_redirect_back()
form = forms.LoginForm()
if form.validate_on_submit():
- if fas.login(form.username.data, form.password.data):
+ if FAS.login(form.username.data, form.password.data):
flask.flash('Welcome, %s' % flask.g.fas_user.username)
return redirect.safe_redirect_back()
else:
@@ -394,7 +383,7 @@ def auth_login():
return flask.render_template('auth/login.html', form=form)
-(a)app.route('/logout')
+(a)APP.route('/logout')
def auth_logout():
if not flask.g.fas_user:
return redirect.safe_redirect_back()
@@ -405,176 +394,217 @@ def auth_logout():
### ELECTION ADMIN VIEWS ###################################
-(a)app.route('/admin/')
+(a)APP.route('/admin/')
@election_admin_required
def admin_view_elections():
- elections = models.Election.query.\
- filter_by(fas_user=flask.g.fas_user.username).all()
- num_elections = len(elections)
- if (num_elections == 0):
+ elections = models.Election.search(
+ SESSION, fas_user=flask.g.fas_user.username)
+
+ if not elections:
flask.flash('You do not have any elections.')
return redirect.safe_redirect_back()
- return flask.render_template('admin/all_elections.html',
- num_elections=num_elections,
- elections=elections)
+ return flask.render_template(
+ 'admin/all_elections.html',
+ elections=elections)
-(a)app.route('/admin/new', methods=('GET', 'POST'))
+(a)APP.route('/admin/new', methods=('GET', 'POST'))
@election_admin_required
def admin_new_election():
form = forms.ElectionForm()
if form.validate_on_submit():
- election = models.Election(**remove_csrf(form.data))
+
+ election = models.Election(
+ summary=form.summary.data,
+ alias=form.alias.data,
+ description=form.description.data,
+ url=form.url.data,
+ start_date=form.start_date.data,
+ end_date=form.end_date.data,
+ number_elected=form.number_elected.data,
+ embargoed=form.embargoed.data,
+ frontpage=form.frontpage.data,
+ voting_type=form.voting_type.data,
+ candidates_are_fasusers=form.candidates_are_fasusers.data,
+ fas_user = flask.g.fas_user.username,
+ )
+
# Fix start_date and end_date to use datetime
election.start_date = datetime.combine(election.start_date, time())
election.end_date = datetime.combine(election.end_date,
time(23, 59, 59))
- election.fas_user = flask.g.fas_user.username
- db.session.add(election)
- admin = models.ElectionAdminGroup(election=election,
- group_name=app.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
- role_required=u'user')
- db.session.add(admin)
- db.session.commit()
+ SESSION.add(election)
+
+ admin = models.ElectionAdminGroup(
+ election=election,
+ group_name=APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ role_required=u'user')
+ SESSION.add(admin)
+
+ SESSION.commit()
+
flask.flash('Election "%s" added' % election.summary)
fedmsgshim.publish(topic="election.new", msg=election)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/election_form.html', form=form,
- submit_text='Create election')
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+ return flask.render_template(
+ 'admin/election_form.html',
+ form=form,
+ submit_text='Create election')
-(a)app.route('/admin/<election_alias>/')
+(a)APP.route('/admin/<election_alias>/')
@election_admin_required
def admin_view_election(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
+ election = models.Election.get(SESSION, alias=election_alias)
if not election:
flask.abort(404)
- return flask.render_template('admin/view_election.html', election=election)
+
+ return flask.render_template(
+ 'admin/view_election.html',
+ election=election)
-(a)app.route('/admin/<election_alias>/edit', methods=('GET', 'POST'))
+(a)APP.route('/admin/<election_alias>/edit', methods=('GET', 'POST'))
@election_admin_required
def admin_edit_election(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
+ election = models.Election.get(SESSION, alias=election_alias)
if not election:
flask.abort(404)
+
form = forms.ElectionForm(election.id, obj=election)
if form.validate_on_submit():
form.populate_obj(election)
- db.session.commit()
+ SESSION.commit()
flask.flash('Election "%s" saved' % election.summary)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/election_form.html', form=form,
- submit_text='Edit election')
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+ return flask.render_template(
+ 'admin/election_form.html',
+ form=form,
+ submit_text='Edit election')
-(a)app.route('/admin/<election_alias>/candidates/new', methods=('GET', 'POST'))
+
+(a)APP.route('/admin/<election_alias>/candidates/new', methods=('GET', 'POST'))
@election_admin_required
def admin_add_candidate(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
+ election = models.Election.get(SESSION, alias=election_alias)
if not election:
flask.abort(404)
+
form = forms.CandidateForm()
if form.validate_on_submit():
- candidate = models.Candidate(election=election,
- **remove_csrf(form.data))
- db.session.add(candidate)
- db.session.commit()
+ candidate = models.Candidate(
+ election=election,
+ name=form.name.data,
+ url=form.url.data
+ )
+
+ SESSION.add(candidate)
+ SESSION.commit()
flask.flash('Candidate "%s" saved' % candidate.name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/candidate.html', form=form,
- submit_text='Add candidate')
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+ return flask.render_template(
+ 'admin/candidate.html',
+ form=form,
+ submit_text='Add candidate')
-(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
+
+(a)APP.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
methods=('GET', 'POST'))
@election_admin_required
def admin_edit_candidate(election_alias, candidate_id):
- election = models.Election.query.filter_by(alias=election_alias).first()
- candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ election = models.Election.get(SESSION, alias=election_alias)
+ candidate = models.Candidate.get(SESSION, candidate_id=candidate_id)
+
if not election or not candidate:
flask.abort(404)
+
if candidate.election != election:
flask.abort(404)
+
form = forms.CandidateForm(obj=candidate)
if form.validate_on_submit():
form.populate_obj(candidate)
- db.session.commit()
+ SESSION.commit()
flask.flash('Candidate "%s" saved' % candidate.name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/candidate.html', form=form,
- submit_text='Edit candidate')
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+
+ return flask.render_template(
+ 'admin/candidate.html',
+ form=form,
+ submit_text='Edit candidate')
-(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
+(a)APP.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
methods=('GET', 'POST'))
@election_admin_required
def admin_delete_candidate(election_alias, candidate_id):
- election = models.Election.query.filter_by(alias=election_alias).first()
- candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ election = models.Election.get(SESSION, alias=election_alias)
+ candidate = models.Candidate.get(SESSION, candidate_id=candidate_id)
+
if not election or not candidate:
flask.abort(404)
+
if candidate.election != election:
flask.abort(404)
+
form = forms.ConfirmationForm()
if form.validate_on_submit():
- db.session.delete(candidate)
candidate_name = candidate.name
- db.session.commit()
+ SESSION.delete(candidate)
+ SESSION.commit()
flask.flash('Candidate "%s" deleted' % candidate_name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/delete_candidate.html', form=form,
- candidate=candidate)
+ return flask.redirect(flask.url_for(
+ 'admin_view_election', election_alias=election.alias))
+ return flask.render_template(
+ 'admin/delete_candidate.html',
+ form=form,
+ candidate=candidate)
-(a)app.route('/results/<election_alias>')
+
+(a)APP.route('/results/<election_alias>')
def election_results(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
+ election = get_valid_election(election_alias)
- if election.status == 'In progress':
- flask.flash("We are sorry. The results for this election cannot be" \
- " viewed at this time because the election is still in" \
- " progress.")
- return redirect.safe_redirect_back()
+ if not isinstance(election, models.Election):
+ return election
- elif election.status == 'Pending':
- flask.flash("We are sorry. The results for this election cannot be" \
- " viewed at this time because the election has not" \
- " started.")
+ if election.status == 'In progress':
+ flask.flash(
+ 'Sorry but this election is in progress, you may not see its '
+ 'results already.')
return redirect.safe_redirect_back()
elif election.embargoed == 1:
if flask.g.fas_user is None:
- flask.flash("We are sorry. The results for this election" \
- "cannot be viewed because they are currently" \
+ flask.flash("We are sorry. The results for this election"
+ "cannot be viewed because they are currently"
" embargoed pending formal announcement.")
return redirect.safe_redirect_back()
else:
- if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
- flask.g.fas_user.groups:
+ if APP.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
+ flask.g.fas_user.groups:
pass
else:
match = 0
- admingroups = models.ElectionAdminGroup.query.filter_by(
- election_id=election.id).all()
+ admingroups = models.ElectionAdminGroup.by_election_id(
+ SESSION, election_id=election.id)
for admingroup in admingroups:
- if admingroup.group_name in flask.g.fas_user.groups:
+ if admingroup.group_name in flask.g.fas_user.groups:
match = 1
if match == 0:
- flask.flash("We are sorry. The results for this election" \
- "cannot be viewed because they are currently" \
- " embargoed pending formal announcement.")
+ flask.flash(
+ "We are sorry. The results for this election"
+ "cannot be viewed because they are currently"
+ " embargoed pending formal announcement.")
return redirect.safe_redirect_back()
usernamemap = {}
commit 0ca5eebd59c929ec79b493df85810aaa5362f198
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:09:38 2014 +0100
Rework the models.py file to port it to plain SQLAlchemy
Expend the classes as well so that all the querying is done
only in this file an nowhere else in the project
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index b19bd33..48d34d6 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -1,31 +1,111 @@
-from fedora_elections import app, db
+# -*- coding: utf-8 -*-
+
+
+import sqlalchemy as sa
+from sqlalchemy import create_engine
+from sqlalchemy.ext.associationproxy import association_proxy
+from sqlalchemy.ext.compiler import compiles
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import scoped_session
+from sqlalchemy.orm import relation, relationship
+from sqlalchemy.orm import backref
+from sqlalchemy.orm.collections import attribute_mapped_collection
+from sqlalchemy.orm.collections import mapped_collection
+from sqlalchemy.orm.exc import NoResultFound
+from sqlalchemy.sql import and_
+from sqlalchemy.sql.expression import Executable, ClauseElement
+
+BASE = declarative_base()
from datetime import datetime
-class Election(db.Model):
+
+def create_tables(db_url, alembic_ini=None, debug=False):
+ """ Create the tables in the database using the information from the
+ url obtained.
+
+ :arg db_url, URL used to connect to the database. The URL contains
+ information with regards to the database engine, the host to
+ connect to, the user and password and the database name.
+ ie: <engine>://<user>:<password>@<host>/<dbname>
+ :kwarg alembic_ini, path to the alembic ini file. This is necessary
+ to be able to use alembic correctly, but not for the unit-tests.
+ :kwarg debug, a boolean specifying wether we should have the verbose
+ output of sqlalchemy or not.
+ :return a session that can be used to query the database.
+
+ """
+ engine = create_engine(db_url, echo=debug)
+ BASE.metadata.create_all(engine)
+ #engine.execute(collection_package_create_view(driver=engine.driver))
+ if db_url.startswith('sqlite:'):
+ ## Ignore the warning about con_record
+ # pylint: disable=W0613
+ def _fk_pragma_on_connect(dbapi_con, con_record):
+ ''' Tries to enforce referential constraints on sqlite. '''
+ dbapi_con.execute('pragma foreign_keys=ON')
+ sa.event.listen(engine, 'connect', _fk_pragma_on_connect)
+
+ if alembic_ini is not None: # pragma: no cover
+ # then, load the Alembic configuration and generate the
+ # version table, "stamping" it with the most recent rev:
+
+ ## Ignore the warning missing alembic
+ # pylint: disable=F0401
+ from alembic.config import Config
+ from alembic import command
+ alembic_cfg = Config(alembic_ini)
+ command.stamp(alembic_cfg, "head")
+
+ scopedsession = scoped_session(sessionmaker(bind=engine))
+ return scopedsession
+
+
+def create_session(db_url, debug=False, pool_recycle=3600):
+ """ Create the Session object to use to query the database.
+
+ :arg db_url: URL used to connect to the database. The URL contains
+ information with regards to the database engine, the host to connect
+ to, the user and password and the database name.
+ ie: <engine>://<user>:<password>@<host>/<dbname>
+ :kwarg debug: a boolean specifying wether we should have the verbose
+ output of sqlalchemy or not.
+ :return a Session that can be used to query the database.
+
+ """
+ engine = sa.create_engine(db_url,
+ echo=debug,
+ pool_recycle=pool_recycle)
+ scopedsession = scoped_session(sessionmaker(bind=engine))
+ return scopedsession
+
+
+class Election(BASE):
__tablename__ = 'elections'
- id = db.Column(db.Integer, primary_key=True)
- summary = db.Column(db.Unicode(150), unique=True, nullable=False)
- alias = db.Column(db.Unicode(100), unique=True, nullable=False)
- description = db.Column(db.UnicodeText, nullable=False)
- url = db.Column(db.Unicode(250), nullable=False)
- start_date = db.Column(db.DateTime, nullable=False)
- end_date = db.Column(db.DateTime, nullable=False)
- number_elected = db.Column(db.Integer, nullable=False, default=1)
- embargoed = db.Column(db.Boolean, nullable=False, default=True)
- frontpage = db.Column(db.Boolean, nullable=False, default=False)
- voting_type = db.Column(db.Unicode(100), nullable=False, default=u'range')
- candidates_are_fasusers = db.Column(db.Boolean, nullable=False,
+ id = sa.Column(sa.Integer, primary_key=True)
+ summary = sa.Column(sa.Unicode(150), unique=True, nullable=False)
+ alias = sa.Column(sa.Unicode(100), unique=True, nullable=False)
+ description = sa.Column(sa.UnicodeText, nullable=False)
+ url = sa.Column(sa.Unicode(250), nullable=False)
+ start_date = sa.Column(sa.DateTime, nullable=False)
+ end_date = sa.Column(sa.DateTime, nullable=False)
+ number_elected = sa.Column(sa.Integer, nullable=False, default=1)
+ embargoed = sa.Column(sa.Boolean, nullable=False, default=True)
+ frontpage = sa.Column(sa.Boolean, nullable=False, default=False)
+ voting_type = sa.Column(sa.Unicode(100), nullable=False, default=u'range')
+ candidates_are_fasusers = sa.Column(sa.Boolean, nullable=False,
default=False)
- fas_user = db.Column(db.Unicode(50), nullable=False)
+ fas_user = sa.Column(sa.Unicode(50), nullable=False)
@property
def status(self):
- if datetime.now() < self.start_date:
+ now = datetime.utcnow()
+ if now < self.start_date:
return 'Pending'
else:
- if datetime.now() < self.end_date:
+ if now < self.end_date:
return 'In progress'
else:
if self.embargoed:
@@ -37,67 +117,160 @@ class Election(db.Model):
def locked(self):
return datetime.now() >= self.start_date
+ @classmethod
+ def search(cls, session, frontpage=None, alias=None, summary=None,
+ fas_user=None):
+ """ Search the election and filter based on the arguments passed.
+ """
+ query = session.query(cls)
+
+ if frontpage is not None:
+ query = query.filter(cls.frontpage == frontpage)
+
+ if alias is not None:
+ query = query.filter(cls.alias == alias)
-class ElectionAdminGroup(db.Model):
+ if summary is not None:
+ query = query.filter(cls.summary == summary)
+
+ if fas_user is not None:
+ query = query.filter(cls.fas_user == fas_user)
+
+ return query.all()
+
+ @classmethod
+ def by_alias(cls, session, alias):
+ """ Return a specific election based on its alias. """
+ return session.query(cls).filter(cls.alias == alias).first()
+
+ get = by_alias
+
+ @classmethod
+ def get_older_election(cls, session, limit):
+ """ Return all the election which end_date if older than the
+ provided limit.
+ """
+ query = session.query(
+ cls
+ ).filter(
+ cls.end_date < limit
+ ).order_by(
+ cls.start_date
+ )
+
+ return query.all()
+
+ @classmethod
+ def get_open_election(cls, session, limit):
+ """ Return all the election which end_date if greater or equal to
+ the provided limit.
+ """
+ query = session.query(
+ cls
+ ).filter(
+ cls.end_date >= limit
+ ).order_by(
+ cls.start_date
+ )
+
+ return query.all()
+
+
+class ElectionAdminGroup(BASE):
__tablename__ = 'electionadmingroups'
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ id = sa.Column(sa.Integer, primary_key=True)
+ election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
nullable=False)
- election = db.relationship('Election', backref=db.backref('admin_groups',
- lazy='dynamic'))
- group_name = db.Column(db.Unicode(150), nullable=False)
- role_required = db.Column(db.Enum(u'user', u'sponsor', u'administrator'),
+ election = relationship(
+ 'Election', backref=backref('admin_groups', lazy='dynamic'))
+ group_name = sa.Column(sa.Unicode(150), nullable=False)
+ role_required = sa.Column(sa.Enum(u'user', u'sponsor', u'administrator'),
nullable=False)
-class Candidate(db.Model):
+ @classmethod
+ def by_election_id(cls, session, election_id):
+ """ Return all the ElectionAdminGroup having a specific election_id.
+ """
+ return session.query(
+ cls
+ ).filter(
+ cls.election_id == election_id
+ ).all()
+
+
+
+class Candidate(BASE):
__tablename__ = 'candidates'
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ id = sa.Column(sa.Integer, primary_key=True)
+ election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
nullable=False)
- election = db.relationship('Election', backref=db.backref('candidates',
- lazy='dynamic'))
- name = db.Column(db.Unicode(150), nullable=False) # FAS username if
+ election = relationship(
+ 'Election', backref=backref('candidates', lazy='dynamic'))
+ name = sa.Column(sa.Unicode(150), nullable=False) # FAS username if
# candidates_are_fasusers
- url = db.Column(db.Unicode(250))
+ url = sa.Column(sa.Unicode(250))
@property
def vote_count(self):
values = [v.value for v in self.votes]
return sum(values)
+ @classmethod
+ def by_id(cls, session, candidate_id):
+ """ Return a specific election based on its alias. """
+ return session.query(cls).filter(cls.id == candidate_id).first()
-class LegalVoter(db.Model):
+ get = by_id
+
+
+class LegalVoter(BASE):
__tablename__ = 'legalvoters'
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ id = sa.Column(sa.Integer, primary_key=True)
+ election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
nullable=False)
- election = db.relationship('Election', backref=db.backref('legal_voters',
- lazy='dynamic'))
+ election = relationship(
+ 'Election', backref=backref('legal_voters', lazy='dynamic'))
# special names:
# "cla + one" = cla_done + 1 non-cla group
- group_name = db.Column(db.Unicode(150), nullable=False)
+ group_name = sa.Column(sa.Unicode(150), nullable=False)
-class Vote(db.Model):
+class Vote(BASE):
__tablename__ = 'votes'
- __table_args__ = (db.UniqueConstraint('election_id', 'voter',
+ __table_args__ = (sa.UniqueConstraint('election_id', 'voter',
'candidate_id',
name='eid_voter_cid'), {} )
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ id = sa.Column(sa.Integer, primary_key=True)
+ election_id = sa.Column(sa.Integer, sa.ForeignKey('elections.id'),
nullable=False)
- election = db.relationship('Election', backref=db.backref('votes',
- lazy='dynamic'))
- voter = db.Column(db.Unicode(150), nullable=False)
- timestamp = db.Column(db.DateTime, nullable=False)
- candidate_id = db.Column(db.Integer, db.ForeignKey('candidates.id'),
+ election = relationship(
+ 'Election', backref=backref('votes', lazy='dynamic'))
+ voter = sa.Column(sa.Unicode(150), nullable=False)
+ timestamp = sa.Column(sa.DateTime, nullable=False)
+ candidate_id = sa.Column(sa.Integer, sa.ForeignKey('candidates.id'),
nullable=False)
- candidate = db.relationship('Candidate', backref=db.backref('votes',
- lazy='dynamic'))
- value = db.Column(db.Integer, nullable=False)
+ candidate = relationship(
+ 'Candidate', backref=backref('votes', lazy='dynamic'))
+ value = sa.Column(sa.Integer, nullable=False)
+
+ @classmethod
+ def of_user_on_election(cls, session, user, election_id, count=False):
+ """ Return the votes of a user on a specific election.
+ If count if True, then return the number of votes instead of the
+ actual votes.
+ """
+ query = session.query(
+ cls
+ ).filter(
+ cls.election_id == election_id
+ ).filter(
+ cls.voter == user
+ )
+
+ return query.all()
commit 1ddffc428f5415f2e0a6cd395673046bccae807e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:08:42 2014 +0100
We cannot have 0 elections, this is taken care of in the controller
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index ae210e0..b25a469 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -1,6 +1,5 @@
{% extends "base.html" %}
{% block body %}
- {% if num_elections > 0 %}
<h2>{{ title }}</h2>
<table>
<tr>
@@ -32,5 +31,4 @@
</tr>
{% endfor %}
</table>
- {% endif %}
{% endblock %}
commit 748c938561ef7da13941a9cafbad0a5391193a71
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:08:39 2014 +0100
We cannot have 0 elections, this is taken care of in the controller
diff --git a/fedora_elections/templates/election/archive.html b/fedora_elections/templates/election/archive.html
index 8740b3c..9874dd5 100644
--- a/fedora_elections/templates/election/archive.html
+++ b/fedora_elections/templates/election/archive.html
@@ -1,28 +1,26 @@
{% extends "base.html" %}
{% block body %}
- {% if num_elections > 0 %}
- <h2>Past Elections</h2>
- <table border="1" cellpadding="1">
+ <h2>Past Elections</h2>
+ <table border="1" cellpadding="1">
+ <tr>
+ <th></th>
+ <th>Start Date (UTC)</th>
+ <th>End Date (UTC)</th>
+ <th></th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
<tr>
- <th></th>
- <th>Start Date (UTC)</th>
- <th>End Date (UTC)</th>
- <th></th>
- <th></th>
+ <td>{{election.description}}</td>
+ <td>{{election.start_date}}</td>
+ <td>{{election.end_date}}</td>
+ <td>
+ <a href="about/{{ election.alias }}">More information</a>
+ </td>
+ <td>
+ <a href="results/{{ election.alias }}">Results</a>
+ </td>
</tr>
- {% for election in elections %}
- <tr>
- <td>{{election.description}}</td>
- <td>{{election.start_date}}</td>
- <td>{{election.end_date}}</td>
- <td>
- <a href="about/{{ election.alias }}"">More information</a>
- </td>
- <td>
- <a href="results/{{ election.alias }}">Results</a>
- </td>
- </tr>
- {% endfor %}
- </table>
- {% endif %}
+ {% endfor %}
+ </table>
{% endblock %}
commit 54b5bfaf7c03aaee66e89671370cebe9b463b783
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:08:32 2014 +0100
We cannot have 0 elections, this is taken care of in the controller
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index e168ac0..20b0d7a 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -1,7 +1,6 @@
{% extends "base.html" %}
{% block title %}Election administration{% endblock %}
{% block body %}
- {% if num_elections > 0 %}
<h2>Election administration</h2>
<table>
<tr>
@@ -24,5 +23,4 @@
</tr>
{% endfor %}
</table>
- {% endif %}
{%endblock %}
commit 0a3939782f9e8c6a64887ff67dfdc9eb87983da8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:08:15 2014 +0100
Remove queries from the form
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 8fddf23..5b7d838 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -1,6 +1,7 @@
from flask.ext import wtf
import wtforms
+from fedora_elections import SESSION
from fedora_elections.models import Election
@@ -62,7 +63,7 @@ class ElectionForm(wtf.Form):
form._election_id = election_id
def validate_summary(form, field):
- check = Election.query.filter_by(summary=form.summary.data).all()
+ check = Election.search(SESSION, summary=form.summary.data)
if check:
if not (form._election_id and form._election_id == check[0].id):
raise wtforms.ValidationError(
@@ -72,7 +73,7 @@ class ElectionForm(wtf.Form):
if form.alias.data == 'new':
raise wtforms.ValidationError(flask.Markup(
'The alias cannot be <code>new</code>.'))
- check = Election.query.filter_by(alias=form.alias.data).all()
+ check = Election.search(SESSION, alias=form.alias.data)
if check:
if not (form._election_id and form._election_id == check[0].id):
raise wtforms.ValidationError(
commit 1c2bab83521827b9e442d5343d52bd0de86e42ee
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 13:22:49 2014 +0100
Remove un-necessary configuraiton file now that everything is moved to the default_config
diff --git a/development.cfg b/development.cfg
deleted file mode 100644
index e4b83bb..0000000
--- a/development.cfg
+++ /dev/null
@@ -1,18 +0,0 @@
-import os, os.path
-
-DEBUG = True
-
-
-# You will want to change this for your install
-SECRET_KEY = 'change me'
-SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.getcwd(), 'database.sqlite')
-
-FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
-FAS_USERNAME = 'elections'
-FAS_PASSWORD ='el3ctMEn0w'
-FAS_CHECK_CERT = False
-FAS_HTTPS_REQUIRED = False
-
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
-# List of URLs for additional site-wide CSS
-EXTRA_CSS = []
diff --git a/development.cfg.sample b/development.cfg.sample
deleted file mode 100644
index 1dff33a..0000000
--- a/development.cfg.sample
+++ /dev/null
@@ -1,18 +0,0 @@
-import os, os.path
-
-DEBUG = True
-
-
-# You will want to change this for your install
-SECRET_KEY = 'change me'
-SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.getcwd(), 'database.sqlite')
-
-FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
-FAS_USERNAME = 'elections'
-FAS_PASSWORD ='fake_fas_password'
-FAS_CHECK_CERT = False
-FAS_HTTPS_REQUIRED = False
-
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
-# List of URLs for additional site-wide CSS
-EXTRA_CSS = []
diff --git a/fedora_elections/fedora-elections.cfg.in b/fedora_elections/fedora-elections.cfg.in
deleted file mode 100644
index 4b49118..0000000
--- a/fedora_elections/fedora-elections.cfg.in
+++ /dev/null
@@ -1,18 +0,0 @@
-import os, os.path
-
-DEBUG = True
-
-
-# You will want to change this for your install
-SECRET_KEY = 'change me'
-SQLALCHEMY_DATABASE_URI = 'sqlite:///@SHAREDSTDIR@/fedora_elections/database.sqlite'
-
-FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
-FAS_USERNAME = 'fcelections'
-FAS_PASSWORD = 'el3cTo$hozer'
-FAS_CHECK_CERT = False
-FAS_FLASK_COOKIE_REQUIRES_HTTPS = False
-
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
-# List of URLs for additional site-wide CSS
-EXTRA_CSS = []
commit f7db6051a929c4b3b803275f473c7086806b0727
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 13:21:54 2014 +0100
Populate the default config with the values present in the sample development configuration file
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
index 7b5b794..3afb452 100644
--- a/fedora_elections/default_config.py
+++ b/fedora_elections/default_config.py
@@ -1 +1,18 @@
FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+
+# url to the database server:
+DB_URL = 'sqlite:////var/tmp/elections_dev.sqlite'
+
+
+# You will want to change this for your install
+SECRET_KEY = 'change me'
+
+FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
+FAS_USERNAME = 'elections'
+FAS_PASSWORD ='el3ctMEn0w'
+FAS_CHECK_CERT = False
+FAS_HTTPS_REQUIRED = False
+
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+# List of URLs for additional site-wide CSS
+EXTRA_CSS = []
commit 4adfcbf2e1280ea51bf8695a7a69363c380dcdd7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 13:21:20 2014 +0100
Remove the sample createdb.in and the corresponding python script
diff --git a/fedora_elections/createdb.in b/fedora_elections/createdb.in
deleted file mode 100644
index 46fabad..0000000
--- a/fedora_elections/createdb.in
+++ /dev/null
@@ -1 +0,0 @@
-FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora-elections/fedora-elections.cfg python @DATADIR(a)/fedora_elections/createdb.py
diff --git a/fedora_elections/createdb.py b/fedora_elections/createdb.py
deleted file mode 100644
index a909c5e..0000000
--- a/fedora_elections/createdb.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7']
-import pkg_resources
-
-from fedora_elections import db
-db.create_all()
commit ee3b59733095b3dd757b94652b1705275b0b5e4a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 13:07:48 2014 +0100
Do not run on port 80 by default as this will conflict with any apache server running
diff --git a/runserver.py b/runserver.py
index b9103e0..9c845ba 100644
--- a/runserver.py
+++ b/runserver.py
@@ -3,4 +3,4 @@ __main__.__requires__ = ['SQLAlchemy >= 0.7']
import pkg_resources
from fedora_elections import app
-app.run(host='0.0.0.0', port=80)
+app.run()
commit ac8ecdaec2a4b30752583cfce4e79b65e0f4f730
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 13:01:49 2014 +0100
Fix the forms to use directly wtforms instead of flask-wtf
There has been a release of flask-wtf that stopped providing the interface to wtforms.
This commit allows us to deal with these changes in flask-wtf
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
index 0f769a2..8fddf23 100644
--- a/fedora_elections/forms.py
+++ b/fedora_elections/forms.py
@@ -1,36 +1,61 @@
from flask.ext import wtf
+import wtforms
from fedora_elections.models import Election
class ElectionForm(wtf.Form):
- summary = wtf.TextField('Summary', [wtf.validators.Required(),
- wtf.validators.Length(max=150)])
- alias = wtf.TextField('Alias', [
- wtf.validators.Required(),
- wtf.validators.Length(max=100),
- wtf.validators.Regexp('[a-z0-9_-]+',
- message=('Alias may only contain lower case '
- 'letters, numbers, hyphens and '
- 'underscores.')),
+ summary = wtforms.TextField(
+ 'Summary', [
+ wtforms.validators.Required(),
+ wtforms.validators.Length(max=150)])
+
+ alias = wtforms.TextField(
+ 'Alias', [
+ wtforms.validators.Required(),
+ wtforms.validators.Length(max=100),
+ wtforms.validators.Regexp(
+ '[a-z0-9_-]+',
+ message=('Alias may only contain lower case letters, numbers, '
+ 'hyphens and underscores.')),
])
- description = wtf.TextAreaField('Description', [wtf.validators.Required()])
- voting_type = wtf.RadioField('Type',
- choices=[('range', 'Range Voting'),
- ('simple', 'Simple Voting')],
- default='range')
- url = wtf.TextField('URL', [wtf.validators.Required(),
- wtf.validators.URL(),
- wtf.validators.Length(max=250)])
- start_date = wtf.DateField('Start date', [wtf.validators.Required()])
- end_date = wtf.DateField('End date', [wtf.validators.Required()])
- number_elected = wtf.IntegerField('Number elected',
- [wtf.validators.Required(),
- wtf.validators.NumberRange(min=1)],
- default=1)
- candidates_are_fasusers = wtf.BooleanField('Candidates are FAS users?')
- frontpage = wtf.BooleanField('Show on front page?')
- embargoed = wtf.BooleanField('Embargo results?', default=True)
+
+ description = wtforms.TextAreaField(
+ 'Description', [
+ wtforms.validators.Required()])
+
+ voting_type = wtforms.RadioField(
+ 'Type',
+ choices=[('range', 'Range Voting'),
+ ('simple', 'Simple Voting')],
+ default='range')
+
+ url = wtforms.TextField(
+ 'URL', [
+ wtforms.validators.Required(),
+ wtforms.validators.URL(),
+ wtforms.validators.Length(max=250)])
+
+ start_date = wtforms.DateField(
+ 'Start date', [
+ wtforms.validators.Required()])
+
+ end_date = wtforms.DateField(
+ 'End date', [
+ wtforms.validators.Required()])
+
+ number_elected = wtforms.IntegerField(
+ 'Number elected',[
+ wtforms.validators.Required(),
+ wtforms.validators.NumberRange(min=1)],
+ default=1)
+
+ candidates_are_fasusers = wtforms.BooleanField(
+ 'Candidates are FAS users?')
+
+ frontpage = wtforms.BooleanField('Show on front page?')
+
+ embargoed = wtforms.BooleanField('Embargo results?', default=True)
def __init__(form, election_id=None, *args, **kwargs):
super(ElectionForm, form).__init__(*args, **kwargs)
@@ -40,33 +65,35 @@ class ElectionForm(wtf.Form):
check = Election.query.filter_by(summary=form.summary.data).all()
if check:
if not (form._election_id and form._election_id == check[0].id):
- raise wtf.ValidationError('There is already another election '
- 'with this summary.')
+ raise wtforms.ValidationError(
+ 'There is already another election with this summary.')
def validate_alias(form, field):
if form.alias.data == 'new':
- raise wtf.ValidationError(flask.Markup('The alias cannot be '
- '<code>new</code>.'))
+ raise wtforms.ValidationError(flask.Markup(
+ 'The alias cannot be <code>new</code>.'))
check = Election.query.filter_by(alias=form.alias.data).all()
if check:
if not (form._election_id and form._election_id == check[0].id):
- raise wtf.ValidationError('There is already another election '
- 'with this alias.')
+ raise wtforms.ValidationError(
+ 'There is already another election with this alias.')
def validate_end_date(form, field):
if form.end_date.data <= form.start_date.data:
- raise wtf.ValidationError('End date must be later than '
- 'start date.')
+ raise wtforms.ValidationError(
+ 'End date must be later than start date.')
class CandidateForm(wtf.Form):
- name = wtf.TextField('Name', [wtf.validators.Required()])
- url = wtf.TextField('URL', [wtf.validators.Length(max=250)])
+ name = wtforms.TextField('Name', [wtforms.validators.Required()])
+ url = wtforms.TextField('URL', [wtforms.validators.Length(max=250)])
class LoginForm(wtf.Form):
- username = wtf.TextField('Username', [wtf.validators.Required()])
- password = wtf.PasswordField('Password', [wtf.validators.Required()])
+ username = wtforms.TextField(
+ 'Username', [wtforms.validators.Required()])
+ password = wtforms.PasswordField(
+ 'Password', [wtforms.validators.Required()])
class ConfirmationForm(wtf.Form):
commit d2d4b3b016000e41ad701cc9b9e9643ca7e191e7
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:27:14 2013 +0000
Updates for new version
diff --git a/fedora-elections.spec b/fedora-elections.spec
index 61a39ac..4977cb3 100644
--- a/fedora-elections.spec
+++ b/fedora-elections.spec
@@ -4,7 +4,7 @@
%define eggname fedora_elections
Name: fedora-elections
-Version: 0.1
+Version: 0.2
Release: 1%{?dist}
Summary: Fedora elections application
@@ -70,7 +70,8 @@ fedora-elections is the Fedora Elections application.
%prep
%setup -q
-./configure.py
+cd fedora_elections
+../configure.py
%build
@@ -83,24 +84,27 @@ rm -rf $RPM_BUILD_ROOT
%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT
%{__mkdir_p} %{buildroot}%{_sysconfdir}/%{name}
-%{__install} -m 444 %{name}.cfg %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
+%{__install} -m 444 fedora_elections/%{name}.cfg \
+ %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
%{__mkdir_p} %{buildroot}%{_sysconfdir}/httpd/conf.d
-%{__install} -m 444 httpd-fedora-elections.conf \
+%{__install} -m 444 fedora_elections/httpd-fedora-elections.conf \
%{buildroot}%{_sysconfdir}/httpd/conf.d/httpd-fedora-elections.conf
-%{__mkdir_p} %{buildroot}%{_datadir}/%{name}
-%{__install} -m 544 createdb %{buildroot}%{_datadir}/%{name}/createdb
-%{__install} -m 544 createdb.py %{buildroot}%{_datadir}/%{name}/createdb.py
-%{__install} -m 444 %{name}.wsgi %{buildroot}%{_datadir}/%{name}/%{name}.wsgi
+%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}
+%{__install} -m 544 fedora_elections/createdb %{buildroot}%{_datadir}/%{modname}/createdb
+%{__install} -m 544 fedora_elections/createdb.py \
+ %{buildroot}%{_datadir}/%{modname}/createdb.py
+%{__install} -m 444 fedora_elections/%{name}.wsgi \
+ %{buildroot}%{_datadir}/%{modname}/%{name}.wsgi
-%{__mkdir_p} %{buildroot}%{_datadir}/%{name}/static
-%{__install} -m 444 fedora_elections/static/* %{buildroot}%{_datadir}/%{name}/static
+%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/static
+%{__install} -m 444 fedora_elections/static/* %{buildroot}%{_datadir}/%{modname}/static
-%{__mkdir_p} %{buildroot}%{_datadir}/%{name}/images
-%{__install} -m 444 fedora_elections/images/* %{buildroot}%{_datadir}/%{name}/images
+%{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/images
+%{__install} -m 444 fedora_elections/images/* %{buildroot}%{_datadir}/%{modname}/images
-%{__mkdir_p} %{buildroot}%{_sharedstatedir}/%{name}
+%{__mkdir_p} %{buildroot}%{_sharedstatedir}/%{modname}
%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates
%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/admin
@@ -127,9 +131,8 @@ rm -rf $RPM_BUILD_ROOT
%defattr(-,root,root,-)
%doc
%dir %{_sysconfdir}/%{name}
-%dir %{_sharedstatedir}/%{name}
-%attr(744,apache,apache) %{_sharedstatedir}/%{name}
-%{_datadir}/%{name}/
+%attr(744,apache,apache) %{_sharedstatedir}/%{modname}
+%{_datadir}/%{modname}/
%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
%config(noreplace) %{_sysconfdir}/httpd/conf.d/httpd-%{name}.conf
%{python_sitelib}/%{modname}/
@@ -137,14 +140,14 @@ rm -rf $RPM_BUILD_ROOT
%post
-cd %{_sharedstatedir}/%{name}
+cd %{_sharedstatedir}/%{modname}
if [[ ! -f database.sqlite ]]
then
#
# The database does not exist create one.
#
echo " Creating a database."
- %{_datadir}/%{name}/createdb
+ %{_datadir}/%{modname}/createdb
echo " Changing permissions."
chmod 644 database.sqlite
echo " Changing owner."
@@ -153,7 +156,7 @@ fi
%postun
-cd %{_sharedstatedir}/%{name}
+cd %{_sharedstatedir}/%{modname}
if [[ -f database.sqlite ]]
then
#
@@ -164,12 +167,15 @@ then
fi
cd ${_sharedstatedir}
-if [[ -d %{_sharedstatedir}/%{name} ]]
+if [[ -d %{_sharedstatedir}/%{modname} ]]
then
- /bin/rmdir %{_sharedstatedir}/%{name}
+ /bin/rmdir %{_sharedstatedir}/%{modname}
fi
%changelog
+* Tue Sep 02 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.2
+- Simple voting
+
* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1
- Creation
commit 26afff3ffbdd0f0cd53ff18141512a1115f2b51a
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:26:17 2013 +0000
Update directory path
diff --git a/fedora_elections/fedora-elections.wsgi.in b/fedora_elections/fedora-elections.wsgi.in
index ecdccc9..430cc5c 100644
--- a/fedora_elections/fedora-elections.wsgi.in
+++ b/fedora_elections/fedora-elections.wsgi.in
@@ -11,7 +11,7 @@ import sys
sys.stdout = sys.stderr
## Set the environment variable pointing to the configuration file
-os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora_elections/fedora-elections.cfg'
+os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
## The following is only needed if you did not install
## as a python module (for example if you run it from a git clone).
commit 37da55e6f768ee1395d6e162a2c191b2c5f532ef
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:25:36 2013 +0000
Update directory path
diff --git a/fedora_elections/createdb.in b/fedora_elections/createdb.in
index e921d8a..46fabad 100644
--- a/fedora_elections/createdb.in
+++ b/fedora_elections/createdb.in
@@ -1 +1 @@
-FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora_elections/fedora-elections.cfg python @DATADIR(a)/fedora_elections/createdb.py
+FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora-elections/fedora-elections.cfg python @DATADIR(a)/fedora_elections/createdb.py
commit 339ee8189509eb65c2184855abece849df001322
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:24:36 2013 +0000
Update version
diff --git a/setup.py b/setup.py
index 83a6354..c1ac01e 100644
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ import pkg_resources
from setuptools import setup, find_packages
setup( name = 'fedora-elections'
- , version = '0.1'
+ , version = '0.2'
, author = 'Frank Chiulli'
, author_email = 'fchiulli(a)fedoraproject.org'
, packages = find_packages()
commit aeea827b5e845a2a847209a99a19cd304a6008f0
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:24:12 2013 +0000
Update task list
diff --git a/TODO b/TODO
index 9573c62..bffc060 100644
--- a/TODO
+++ b/TODO
@@ -3,10 +3,7 @@ Things that need doing to get this deployed
* Add legalvoters to an election
* Write a conversion script for the old db model to the new one
-* rpmify
* rpmify flask-fas (or get flask-fas into python-fedora)
-* Implement a voting method for range voting
-* Implement a voting method for non-range voting
* CSRF protection -- anything going through wtf forms is protected. It's up to
us to make sure that no data changing requests are coded without wtf forms.
commit e4c7f7ae7c2afe2f478687739e1c39f6433ee15c
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:23:57 2013 +0000
Add info to clone git repo
diff --git a/README b/README
index c768ad2..4ab94e0 100644
--- a/README
+++ b/README
@@ -47,6 +47,8 @@ Get the source code
You can obtain the code from github:
https://github.com/fedora-infra/elections.git
+git clone https://github.com/fedora-infra/elections.git
+
Configure the application
-------------------------
commit 90ca09bdfb7b28b7d399a240ea01fe4f0e078b7f
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Mon Sep 2 21:00:46 2013 +0000
More simple voting updates
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 86bfdc6..c6e5f21 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -186,7 +186,45 @@ def vote(election_alias):
elif election.status == 'Ended':
flask.flash('This election is closed. You have been redirected to ' +
'the election results.')
- return flask.redirect(flask.url_for('results', election.shortname))
+ return flask.redirect(flask.url_for('election_results',
+ election_alias=election.alias))
+
+ votes = models.Vote.query.filter_by(election_id=election.id,
+ voter=flask.g.fas_user.username).count()
+ if (votes != 0):
+ flask.flash('You have already voted in the election!')
+ return redirect.safe_redirect_back()
+
+ if (election.voting_type == 'range'):
+ return vote_range(election_alias)
+ elif (election.voting_type == 'simple'):
+ return vote_simple(election_alias)
+ else:
+ flask.flash('Unknown election voting type: %s' % election.voting_type)
+ return redirect.safe_redirect_back()
+
+
+(a)app.route('/vote_range/<election_alias>', methods=['GET', 'POST'])
+def vote_range(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'Pending':
+ flask.flash('Voting has not yet started, sorry.')
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Ended':
+ flask.flash('This election is closed. You have been redirected to ' +
+ 'the election results.')
+ return flask.redirect(flask.url_for('election_results',
+ election_alias=election.alias))
+
+ if (election.voting_type == 'simple'):
+ return flask.redirect(flask.url_for('vote_simple',
+ election_alias=election_alias))
votes = models.Vote.query.filter_by(election_id=election.id,
voter=flask.g.fas_user.username).count()
@@ -251,7 +289,6 @@ def vote(election_alias):
if (next_action != 'vote'):
next_action = 'confirm'
-
usernamemap = {}
if (election.candidates_are_fasusers):
for candidate in election.candidates:
@@ -262,11 +299,84 @@ def vote(election_alias):
# User has their name set to private or user doesn't exist.
usernamemap[candidate.id] = candidate.name
- return flask.render_template('election/vote.html', election=election,
+ return flask.render_template('election/vote_range.html', election=election,
num_candidates=num_candidates,
usernamemap=usernamemap,
voteinfo=uvotes, nextaction=next_action)
+(a)app.route('/vote_simple/<election_alias>', methods=['GET', 'POST'])
+def vote_simple(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'Pending':
+ flask.flash('Voting has not yet started, sorry.')
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Ended':
+ flask.flash('This election is closed. You have been redirected to ' +
+ 'the election results.')
+ return flask.redirect(flask.url_for('election_results',
+ election_alias=election_alias))
+
+ if (election.voting_type == 'range'):
+ return flask.redirect(flask.url_for('vote_range',
+ election_alias=election_alias))
+
+ votes = models.Vote.query.filter_by(election_id=election.id,
+ voter=flask.g.fas_user.username).count()
+ if (votes != 0):
+ flask.flash('You have already voted in the election!')
+ return redirect.safe_redirect_back()
+
+ num_candidates = election.candidates.count()
+ next_action = 'vote'
+ candidate_id = -1
+
+ if flask.request.method == 'POST':
+ next_action = ''
+ form_values = flask.request.form.values()
+ if 'Submit' in form_values:
+ candidate_id = int(flask.request.form['candidate'])
+ new_vote = models.Vote(election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=candidate_id,
+ value=1 )
+ db.session.add(new_vote)
+ db.session.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return redirect.safe_redirect_back()
+
+ elif 'Preview' in form_values:
+ if ('candidate' in flask.request.form):
+ flask.flash("Please confirm your vote!")
+ candidate_id = int(flask.request.form['candidate'])
+ else:
+ flask.flash("Please vote for a candidate")
+ next_action = 'vote'
+
+ if (next_action != 'vote'):
+ next_action = 'confirm'
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/vote_simple.html', election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ candidate_id=candidate_id, nextaction=next_action)
### AUTH VIEWS #############################################
@@ -310,7 +420,7 @@ def admin_view_elections():
elections=elections)
-(a)app.route('/admin/new', methods=("GET", "POST"))
+(a)app.route('/admin/new', methods=('GET', 'POST'))
@election_admin_required
def admin_new_election():
form = forms.ElectionForm()
@@ -344,7 +454,7 @@ def admin_view_election(election_alias):
return flask.render_template('admin/view_election.html', election=election)
-(a)app.route('/admin/<election_alias>/edit', methods=("GET", "POST"))
+(a)app.route('/admin/<election_alias>/edit', methods=('GET', 'POST'))
@election_admin_required
def admin_edit_election(election_alias):
election = models.Election.query.filter_by(alias=election_alias).first()
@@ -361,7 +471,7 @@ def admin_edit_election(election_alias):
submit_text='Edit election')
-(a)app.route('/admin/<election_alias>/candidates/new', methods=("GET", "POST"))
+(a)app.route('/admin/<election_alias>/candidates/new', methods=('GET', 'POST'))
@election_admin_required
def admin_add_candidate(election_alias):
election = models.Election.query.filter_by(alias=election_alias).first()
@@ -381,7 +491,7 @@ def admin_add_candidate(election_alias):
@app.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
- methods=("GET", "POST"))
+ methods=('GET', 'POST'))
@election_admin_required
def admin_edit_candidate(election_alias, candidate_id):
election = models.Election.query.filter_by(alias=election_alias).first()
@@ -402,7 +512,7 @@ def admin_edit_candidate(election_alias, candidate_id):
@app.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
- methods=("GET", "POST"))
+ methods=('GET', 'POST'))
@election_admin_required
def admin_delete_candidate(election_alias, candidate_id):
election = models.Election.query.filter_by(alias=election_alias).first()
diff --git a/fedora_elections/templates/election/vote.html b/fedora_elections/templates/election/vote.html
deleted file mode 100644
index e3c4dd4..0000000
--- a/fedora_elections/templates/election/vote.html
+++ /dev/null
@@ -1,116 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-<h2>{{election.summary}}</h2>
-
-<p>{{election.description}}</p>
-
-<p><a href="{{election.url}}">[More Information]</a></p>
-
-<form action="" method="post">
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- <a href="{{candidate.url}}">[Info]</a>
- </td>
- <!-- If user is logged in: -->
- {% if g.fas_user %}
- {% if nextaction == 'vote' %}
- <td>
- <select name="{{candidate.id}}">
- {% for opt in range(0, num_candidates+1) %}
- <option value={{opt}}>{{opt}}</option>
- {% endfor %}
- </select>
- </td>
- {% endif %}
- {% if nextaction == 'confirm' %}
- <td>
- {% set cid = candidate.id %}
- <input type="hidden" name="{{candidate.id}}"
- value="{{voteinfo[cid]}}" />
- {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
- {% if weightpercent >= 90 %}
- <div class="vthot">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 70 %}
- <div class="vtwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 50 %}
- <div class="vtmedwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 30 %}
- <div class="vtmedcool">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 10 %}
- <div class="vtcool">
- {{voteinfo[cid]}}
- </div>
- {% else %}
- <div class="vtcold">
- {{voteinfo[cid]}}
- </div>
- {% endif %}
- </td>
- {% endif %}
- {% endif %}
- <!-- End if user logged in -->
- </tr>
- {% endfor %}
- </table>
- {% if g.fas_user %}
- <p>
- {% if nextaction == 'confirm' %}
- <input type="submit" name="{{nextaction}}" value="Submit" />
- {% else %}
- <input type="submit" name="{{nextaction}}" value="Preview" />
- {% endif %}
- </p>
- {% endif %}
-</form>
-
-<!-- Make this conditional on whether the user is logged in and can vote in the election -->
-{% if election.legal_voters.count() != 0 %}
-<p>
- To vote in this election you must be a member of any one of the following groups:
-</p>
-<ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
-</ul>
-{% else %}
-<p />
-{% endif %}
-<div>
- <h2>Information on Range Voting</h2>
- <p>
- Fedora Project has implemented
- <a href="http://rangevoting.org/">Range Voting</a>
- for this election, in particular the "Range (score-summing, blanks
- treated as zero score, no quorum rule)" range voting system.
- </p>
- <p>
- To cast your vote in this election simply select a value between 0 and
- {{num_candidates}} with 0 as 'least or no preference' and
- {{num_candidates}} as 'highest preference'.
- </p>
- <p>
- At the end of the election, the highest ranking candidate(s) are marked
- as the winners.
- </p>
- <p>
- For more information about Range Voting, visit the
- <a href="http://rangevoting.org/">Center for Range Voting</a>.
- </p>
-</div>
-{% endblock %}
diff --git a/fedora_elections/templates/election/vote_range.html b/fedora_elections/templates/election/vote_range.html
new file mode 100644
index 0000000..e3b2e84
--- /dev/null
+++ b/fedora_elections/templates/election/vote_range.html
@@ -0,0 +1,116 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{election.summary}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
+<form action="" method="post">
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ <a href="{{candidate.url}}">[Info]</a>
+ </td>
+ <!-- If user is logged in: -->
+ {% if g.fas_user %}
+ {% if nextaction == 'vote' %}
+ <td>
+ <select name="{{candidate.id}}">
+ {% for opt in range(0, num_candidates+1) %}
+ <option value={{opt}}>{{opt}}</option>
+ {% endfor %}
+ </select>
+ </td>
+ {% endif %}
+ {% if nextaction == 'confirm' %}
+ <td>
+ {% set cid = candidate.id %}
+ <input type="hidden" name="{{candidate.id}}"
+ value="{{voteinfo[cid]}}" />
+ {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
+ {% if weightpercent >= 90 %}
+ <div class="vthot">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 70 %}
+ <div class="vtwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 50 %}
+ <div class="vtmedwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 30 %}
+ <div class="vtmedcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 10 %}
+ <div class="vtcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% else %}
+ <div class="vtcold">
+ {{voteinfo[cid]}}
+ </div>
+ {% endif %}
+ </td>
+ {% endif %}
+ {% endif %}
+ <!-- End if user logged in -->
+ </tr>
+ {% endfor %}
+ </table>
+ {% if g.fas_user %}
+ <p>
+ {% if nextaction == 'confirm' %}
+ <input type="submit" name="{{nextaction}}" value="Submit" />
+ {% else %}
+ <input type="submit" name="{{nextaction}}" value="Preview" />
+ {% endif %}
+ </p>
+ {% endif %}
+</form>
+
+<!-- Make this conditional on whether the user is logged in and can vote in the election -->
+{% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+{% else %}
+ <p />
+{% endif %}
+<div>
+ <h2>Information on Range Voting</h2>
+ <p>
+ Fedora Project has implemented
+ <a href="http://rangevoting.org/">Range Voting</a>
+ for this election, in particular the "Range (score-summing, blanks
+ treated as zero score, no quorum rule)" range voting system.
+ </p>
+ <p>
+ To cast your vote in this election simply select a value between 0 and
+ {{num_candidates}} with 0 as 'least or no preference' and
+ {{num_candidates}} as 'highest preference'.
+ </p>
+ <p>
+ At the end of the election, the highest ranking candidate(s) are marked
+ as the winners.
+ </p>
+ <p>
+ For more information about Range Voting, visit the
+ <a href="http://rangevoting.org/">Center for Range Voting</a>.
+ </p>
+</div>
+{% endblock %}
diff --git a/fedora_elections/templates/election/vote_simple.html b/fedora_elections/templates/election/vote_simple.html
new file mode 100644
index 0000000..17434a1
--- /dev/null
+++ b/fedora_elections/templates/election/vote_simple.html
@@ -0,0 +1,53 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{election.summary}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
+<form action="" method="post">
+ <table border="0" cellpadding="1">
+ {% for candidate in election.candidates|sort %}
+ <tr>
+ <td>
+ {% if candidate.id == candidate_id %}
+ <input type="radio" name="candidate" value="{{ candidate.id }}" checked="checked">
+ {% else %}
+ <input type="radio" name="candidate" value="{{ candidate.id }}">
+ {% endif %}
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ <a href="{{candidate.url}}">[Info]</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% if g.fas_user %}
+ <p>
+ {% if nextaction == 'confirm' %}
+ <input type="submit" name="{{nextaction}}" value="Submit" />
+ {% else %}
+ <input type="submit" name="{{nextaction}}" value="Preview" />
+ {% endif %}
+ </p>
+ {% endif %}
+</form>
+
+<!-- Make this conditional on whether the user is logged in and can vote in the election -->
+{% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+{% else %}
+ <p />
+{% endif %}
+{% endblock %}
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 290c2cf..ae210e0 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -19,8 +19,12 @@
<td>
{% if election.status == "In progress" %}
{% if g.fas_user %}
- <a href="{{ url_for('vote', election_alias=election.alias) }}">
- Vote now!
+ {% if election.voting_type == 'range' %}
+ <a href="{{ url_for('vote_range', election_alias=election.alias) }}">
+ {% elif election.voting_type == 'simple' %}
+ <a href="{{ url_for('vote_simple', election_alias=election.alias) }}">
+ {% endif %}
+ Vote now!
</a>
{% endif %}
{% endif %}
commit 075463be8bb5afef58b083dc96b90d7e1e29b158
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Aug 25 20:21:10 2013 +0000
Create simple vote election
diff --git a/README b/README
index 442ebb5..c768ad2 100644
--- a/README
+++ b/README
@@ -50,7 +50,7 @@ You can obtain the code from github:
Configure the application
-------------------------
-cd fedora-elections
+cd fedora_elections
../configure.py --install-conf=${PWD%/*} --install-data=${PWD%/*} --shared-state=${PWD%/*}
This will create createdb, fedora-elections.cfg, fedora-elections.wsgi and
diff --git a/fedora-elections/__init__.py b/fedora-elections/__init__.py
deleted file mode 100644
index 86bfdc6..0000000
--- a/fedora-elections/__init__.py
+++ /dev/null
@@ -1,481 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2012 Red Hat, Inc.
-# Copyright © 2012 Ian Weller <ianweller(a)fedoraproject.org>
-# Copyright © 2012 Toshio Kuratomi <tkuratom(a)redhat.com>
-# Copyright © 2012 Frank Chiulli <fchiulli(a)fedoraproject.org>
-#
-# This copyrighted material is made available to anyone wishing to use, modify,
-# copy, or redistribute it subject to the terms and conditions of the GNU
-# General Public License v.2, or (at your option) any later version. This
-# program is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY expressed or implied, including the implied warranties of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
-# Public License for more details. You should have received a copy of the GNU
-# General Public License along with this program; if not, write to the Free
-# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
-# code or documentation are not subject to the GNU General Public License and
-# may only be used or replicated with the express permission of Red Hat, Inc.
-#
-# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
-# Toshio Kuratomi <tkuratom(a)redhat.com>
-# Frank Chiulli <fchiulli(a)fedoraproject.org>
-#
-
-import flask
-from flask.ext.fas import FAS
-from flask.ext.sqlalchemy import SQLAlchemy
-
-from fedora.client import AuthError, AppError
-from fedora.client.fas2 import AccountSystem
-
-from sqlalchemy.orm.exc import NoResultFound
-
-import fedmsgshim
-
-from datetime import datetime, time
-from functools import wraps
-from urlparse import urlparse, urljoin
-
-app = flask.Flask(__name__)
-app.config.from_object('fedora_elections.default_config')
-app.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
-
-# set up SQLAlchemy
-db = SQLAlchemy(app)
-
-# set up FAS
-fas = FAS(app)
-
-# FAS for usernames.
-# baseURL = "https://admin.fedoraproject.org/accounts"
-# fasuser = "elections"
-# faspw = "elections"
-# fas2_url = 'http://fakefas.fedoraproject.org/accounts/'
-fas2_url = app.config['FAS_BASE_URL']
-fas2_username = app.config['FAS_USERNAME']
-fas2_password = app.config['FAS_PASSWORD']
-fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password,
- insecure=True)
-
-
-# modular imports
-from fedora_elections import forms, models, redirect
-
-
-def remove_csrf(form_data):
- return dict([(k, v) for k, v in form_data.items() if k != 'csrf'])
-
-
-def login_required(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- if flask.g.fas_user is None:
- return flask.redirect(flask.url_for('auth_login',
- next=flask.request.url))
- return f(*args, **kwargs)
- return decorated_function
-
-
-def election_admin_required(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- if flask.g.fas_user is None:
- return flask.redirect(flask.url_for('auth_login',
- next=flask.request.url))
- if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
- flask.g.fas_user.groups:
- flask.abort(403)
- return f(*args, **kwargs)
- return decorated_function
-
-
-### LIST VIEWS #############################################
-
-(a)app.route('/')
-def index():
- elections = models.Election.query.filter_by(frontpage=True).all()
- num_elections = len(elections)
- return flask.render_template('list/index.html', elections=elections,
- num_elections=num_elections,
- title="Elections")
-
-
-(a)app.route('/about/<election_alias>')
-def about_election(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template('election/about.html', election=election,
- usernamemap=usernamemap)
-
-
-(a)app.route('/archive')
-def archived_elections():
- now = datetime.utcnow()
-
- try:
- elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date<now, models.Election.id>0).all()
- except NoResultFound:
- flask.flash('There are no archived elections.')
- return redirect.safe_redirect_back()
-
- num_elections = len(elections)
- if (num_elections == 0):
- flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
-
- return flask.render_template('election/archive.html',
- num_elections=num_elections,
- elections=elections)
-
-
-(a)app.route('/open')
-def open_elections():
- now = datetime.utcnow()
-
- try:
- elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date>now,
- models.Election.id>0).all()
- except NoResultFound:
- flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
-
- num_elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date>now,
- models.Election.id>0).count()
- if (num_elections == 0):
- flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
-
- return flask.render_template('list/index.html', elections=elections,
- num_elections=num_elections,
- title='Open Elections')
-
-
-### ELECTION VIEWS #############################################
-
-(a)app.route('/vote/<election_alias>', methods=['GET', 'POST'])
-def vote(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
-
- if election.status == 'Pending':
- flask.flash('Voting has not yet started, sorry.')
- return redirect.safe_redirect_back()
-
- elif election.status == 'Ended':
- flask.flash('This election is closed. You have been redirected to ' +
- 'the election results.')
- return flask.redirect(flask.url_for('results', election.shortname))
-
- votes = models.Vote.query.filter_by(election_id=election.id,
- voter=flask.g.fas_user.username).count()
- if (votes != 0):
- flask.flash('You have already voted in the election!')
- return redirect.safe_redirect_back()
-
- num_candidates = election.candidates.count()
-
- uvotes = {}
- for candidate in election.candidates:
- uvotes[candidate.id] = ""
- next_action = 'vote'
-
- if flask.request.method == 'POST':
- next_action = ''
- form_values = flask.request.form.values()
- if 'Submit' in form_values:
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote >= 0 and vote <= num_candidates:
- uvotes[candidate.id] = int(vote)
- else:
- flask.flash("Invalid Ballot!")
- return redirect.safe_redirect_back()
-
- except ValueError:
- flask.flash("Invalid Ballot!")
- return redirect.safe_redirect_back()
-
- for uvote in uvotes:
- new_vote = models.Vote(election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=uvote,
- value=uvotes[uvote] )
- db.session.add(new_vote)
- db.session.commit()
-
- flask.flash("Your vote has been recorded. Thank you!")
- return redirect.safe_redirect_back()
-
- elif 'Preview' in form_values:
- flask.flash("Please confirm your vote!")
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote > num_candidates:
- flask.flash("Invalid data.")
- uvotes[candidate.id] = 0
- next_action = 'vote'
- elif vote >= 0:
- uvotes[candidate.id] = vote
- else:
- flask.flash("Invalid data2.")
- uvotes[c.id] = 0
- next_action = 'vote'
- except ValueError:
- flask.flash("Invalid data")
-
- if (next_action != 'vote'):
- next_action = 'confirm'
-
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template('election/vote.html', election=election,
- num_candidates=num_candidates,
- usernamemap=usernamemap,
- voteinfo=uvotes, nextaction=next_action)
-
-
-### AUTH VIEWS #############################################
-
-(a)app.route('/login', methods=('GET', 'POST'))
-def auth_login():
- if flask.g.fas_user:
- return redirect.safe_redirect_back()
- form = forms.LoginForm()
- if form.validate_on_submit():
- if fas.login(form.username.data, form.password.data):
- flask.flash('Welcome, %s' % flask.g.fas_user.username)
- return redirect.safe_redirect_back()
- else:
- flask.flash('Incorrect username or password')
- return flask.render_template('auth/login.html', form=form)
-
-
-(a)app.route('/logout')
-def auth_logout():
- if not flask.g.fas_user:
- return redirect.safe_redirect_back()
- fas.logout()
- flask.flash('You have been logged out')
- return redirect.safe_redirect_back()
-
-
-### ELECTION ADMIN VIEWS ###################################
-
-(a)app.route('/admin/')
-@election_admin_required
-def admin_view_elections():
- elections = models.Election.query.\
- filter_by(fas_user=flask.g.fas_user.username).all()
- num_elections = len(elections)
- if (num_elections == 0):
- flask.flash('You do not have any elections.')
- return redirect.safe_redirect_back()
-
- return flask.render_template('admin/all_elections.html',
- num_elections=num_elections,
- elections=elections)
-
-
-(a)app.route('/admin/new', methods=("GET", "POST"))
-@election_admin_required
-def admin_new_election():
- form = forms.ElectionForm()
- if form.validate_on_submit():
- election = models.Election(**remove_csrf(form.data))
- # Fix start_date and end_date to use datetime
- election.start_date = datetime.combine(election.start_date, time())
- election.end_date = datetime.combine(election.end_date,
- time(23, 59, 59))
- election.fas_user = flask.g.fas_user.username
- db.session.add(election)
- admin = models.ElectionAdminGroup(election=election,
- group_name=app.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
- role_required=u'user')
- db.session.add(admin)
- db.session.commit()
- flask.flash('Election "%s" added' % election.summary)
- fedmsgshim.publish(topic="election.new", msg=election)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/election_form.html', form=form,
- submit_text='Create election')
-
-
-(a)app.route('/admin/<election_alias>/')
-@election_admin_required
-def admin_view_election(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
- if not election:
- flask.abort(404)
- return flask.render_template('admin/view_election.html', election=election)
-
-
-(a)app.route('/admin/<election_alias>/edit', methods=("GET", "POST"))
-@election_admin_required
-def admin_edit_election(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
- if not election:
- flask.abort(404)
- form = forms.ElectionForm(election.id, obj=election)
- if form.validate_on_submit():
- form.populate_obj(election)
- db.session.commit()
- flask.flash('Election "%s" saved' % election.summary)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/election_form.html', form=form,
- submit_text='Edit election')
-
-
-(a)app.route('/admin/<election_alias>/candidates/new', methods=("GET", "POST"))
-@election_admin_required
-def admin_add_candidate(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
- if not election:
- flask.abort(404)
- form = forms.CandidateForm()
- if form.validate_on_submit():
- candidate = models.Candidate(election=election,
- **remove_csrf(form.data))
- db.session.add(candidate)
- db.session.commit()
- flask.flash('Candidate "%s" saved' % candidate.name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/candidate.html', form=form,
- submit_text='Add candidate')
-
-
-(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
- methods=("GET", "POST"))
-@election_admin_required
-def admin_edit_candidate(election_alias, candidate_id):
- election = models.Election.query.filter_by(alias=election_alias).first()
- candidate = models.Candidate.query.filter_by(id=candidate_id).first()
- if not election or not candidate:
- flask.abort(404)
- if candidate.election != election:
- flask.abort(404)
- form = forms.CandidateForm(obj=candidate)
- if form.validate_on_submit():
- form.populate_obj(candidate)
- db.session.commit()
- flask.flash('Candidate "%s" saved' % candidate.name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/candidate.html', form=form,
- submit_text='Edit candidate')
-
-
-(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
- methods=("GET", "POST"))
-@election_admin_required
-def admin_delete_candidate(election_alias, candidate_id):
- election = models.Election.query.filter_by(alias=election_alias).first()
- candidate = models.Candidate.query.filter_by(id=candidate_id).first()
- if not election or not candidate:
- flask.abort(404)
- if candidate.election != election:
- flask.abort(404)
- form = forms.ConfirmationForm()
- if form.validate_on_submit():
- db.session.delete(candidate)
- candidate_name = candidate.name
- db.session.commit()
- flask.flash('Candidate "%s" deleted' % candidate_name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/delete_candidate.html', form=form,
- candidate=candidate)
-
-
-(a)app.route('/results/<election_alias>')
-def election_results(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
-
- if election.status == 'In progress':
- flask.flash("We are sorry. The results for this election cannot be" \
- " viewed at this time because the election is still in" \
- " progress.")
- return redirect.safe_redirect_back()
-
- elif election.status == 'Pending':
- flask.flash("We are sorry. The results for this election cannot be" \
- " viewed at this time because the election has not" \
- " started.")
- return redirect.safe_redirect_back()
-
- elif election.embargoed == 1:
- if flask.g.fas_user is None:
- flask.flash("We are sorry. The results for this election" \
- "cannot be viewed because they are currently" \
- " embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
- else:
- if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
- flask.g.fas_user.groups:
- pass
- else:
- match = 0
- admingroups = models.ElectionAdminGroup.query.filter_by(
- election_id=election.id).all()
- for admingroup in admingroups:
- if admingroup.group_name in flask.g.fas_user.groups:
- match = 1
-
- if match == 0:
- flask.flash("We are sorry. The results for this election" \
- "cannot be viewed because they are currently" \
- " embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template('election/results.html', election=election,
- usernamemap=usernamemap)
diff --git a/fedora-elections/createdb.in b/fedora-elections/createdb.in
deleted file mode 100644
index c7a87da..0000000
--- a/fedora-elections/createdb.in
+++ /dev/null
@@ -1 +0,0 @@
-FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora-elections/fedora-elections.cfg python @DATADIR(a)/fedora-elections/createdb.py
diff --git a/fedora-elections/createdb.py b/fedora-elections/createdb.py
deleted file mode 100644
index a909c5e..0000000
--- a/fedora-elections/createdb.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7']
-import pkg_resources
-
-from fedora_elections import db
-db.create_all()
diff --git a/fedora-elections/default_config.py b/fedora-elections/default_config.py
deleted file mode 100644
index 7b5b794..0000000
--- a/fedora-elections/default_config.py
+++ /dev/null
@@ -1 +0,0 @@
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
diff --git a/fedora-elections/fedmsgshim.py b/fedora-elections/fedmsgshim.py
deleted file mode 100644
index 775f785..0000000
--- a/fedora-elections/fedmsgshim.py
+++ /dev/null
@@ -1,17 +0,0 @@
-""" This is a temporary shim that allows fedmsg to be an optional dependency of
-elections. If fedmsg is installed, these function calls will try to actually send
-messages. If it is not installed, it will return silently.
-
- :Author: Ralph Bean <rbean(a)redhat.com>
-
-"""
-
-import warnings
-
-
-def publish(*args, **kwargs):
- try:
- import fedmsg
- fedmsg.publish(*args, **kwargs)
- except Exception, e:
- warnings.warn(str(e))
diff --git a/fedora-elections/fedora-elections.cfg.in b/fedora-elections/fedora-elections.cfg.in
deleted file mode 100644
index ae68f66..0000000
--- a/fedora-elections/fedora-elections.cfg.in
+++ /dev/null
@@ -1,18 +0,0 @@
-import os, os.path
-
-DEBUG = True
-
-
-# You will want to change this for your install
-SECRET_KEY = 'change me'
-SQLALCHEMY_DATABASE_URI = 'sqlite:///@SHAREDSTDIR@/fedora-elections/database.sqlite'
-
-FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
-FAS_USERNAME = 'fcelections'
-FAS_PASSWORD = 'el3cTo$hozer'
-FAS_CHECK_CERT = False
-FAS_HTTPS_REQUIRED = False
-
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
-# List of URLs for additional site-wide CSS
-EXTRA_CSS = []
diff --git a/fedora-elections/fedora-elections.wsgi.in b/fedora-elections/fedora-elections.wsgi.in
deleted file mode 100644
index 430cc5c..0000000
--- a/fedora-elections/fedora-elections.wsgi.in
+++ /dev/null
@@ -1,21 +0,0 @@
-#-*- coding: UTF-8 -*-
-
-# These two lines are needed to run on EL6
-import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
-import pkg_resources
-
-import os
-import sys
-
-sys.stdout = sys.stderr
-
-## Set the environment variable pointing to the configuration file
-os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
-
-## The following is only needed if you did not install
-## as a python module (for example if you run it from a git clone).
-import sys
-# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
-
-from fedora_elections import app as application
diff --git a/fedora-elections/forms.py b/fedora-elections/forms.py
deleted file mode 100644
index f050dbd..0000000
--- a/fedora-elections/forms.py
+++ /dev/null
@@ -1,69 +0,0 @@
-from flask.ext import wtf
-
-from fedora_elections.models import Election
-
-
-class ElectionForm(wtf.Form):
- summary = wtf.TextField('Summary', [wtf.validators.Required(),
- wtf.validators.Length(max=150)])
- alias = wtf.TextField('Alias', [
- wtf.validators.Required(),
- wtf.validators.Length(max=100),
- wtf.validators.Regexp('[a-z0-9_-]+',
- message=('Alias may only contain lower case '
- 'letters, numbers, hyphens and '
- 'underscores.')),
- ])
- description = wtf.TextAreaField('Description', [wtf.validators.Required()])
- url = wtf.TextField('URL', [wtf.validators.Required(),
- wtf.validators.URL(),
- wtf.validators.Length(max=250)])
- start_date = wtf.DateField('Start date', [wtf.validators.Required()])
- end_date = wtf.DateField('End date', [wtf.validators.Required()])
- number_elected = wtf.IntegerField('Number elected',
- [wtf.validators.Required(),
- wtf.validators.NumberRange(min=1)],
- default=1)
- candidates_are_fasusers = wtf.BooleanField('Candidates are FAS users?')
- frontpage = wtf.BooleanField('Show on front page?')
- embargoed = wtf.BooleanField('Embargo results?', default=True)
-
- def __init__(form, election_id=None, *args, **kwargs):
- super(ElectionForm, form).__init__(*args, **kwargs)
- form._election_id = election_id
-
- def validate_summary(form, field):
- check = Election.query.filter_by(summary=form.summary.data).all()
- if check:
- if not (form._election_id and form._election_id == check[0].id):
- raise wtf.ValidationError('There is already another election '
- 'with this summary.')
-
- def validate_alias(form, field):
- if form.alias.data == 'new':
- raise wtf.ValidationError(flask.Markup('The alias cannot be '
- '<code>new</code>.'))
- check = Election.query.filter_by(alias=form.alias.data).all()
- if check:
- if not (form._election_id and form._election_id == check[0].id):
- raise wtf.ValidationError('There is already another election '
- 'with this alias.')
-
- def validate_end_date(form, field):
- if form.end_date.data <= form.start_date.data:
- raise wtf.ValidationError('End date must be later than '
- 'start date.')
-
-
-class CandidateForm(wtf.Form):
- name = wtf.TextField('Name', [wtf.validators.Required()])
- url = wtf.TextField('URL', [wtf.validators.Length(max=250)])
-
-
-class LoginForm(wtf.Form):
- username = wtf.TextField('Username', [wtf.validators.Required()])
- password = wtf.PasswordField('Password', [wtf.validators.Required()])
-
-
-class ConfirmationForm(wtf.Form):
- pass
diff --git a/fedora-elections/httpd-fedora-elections.conf.in b/fedora-elections/httpd-fedora-elections.conf.in
deleted file mode 100644
index c974b88..0000000
--- a/fedora-elections/httpd-fedora-elections.conf.in
+++ /dev/null
@@ -1,23 +0,0 @@
-Alias /fedora-elections/images @DATADIR@/fedora-elections/images
-Alias /fedora-elections/static @DATADIR@/fedora-elections/static
-
-WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
-WSGISocketPrefix run/wsgi
-WSGIRestrictStdout On
-WSGIRestrictSignal Off
-WSGIPythonOptimize 1
-
-WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora-elections/fedora-elections.wsgi
-
-<Location /fedora-elections>
- WSGIProcessGroup fedora-elections
- <IfModule mod_authz_core.c>
- # Apache 2.4
- Require all granted
- </IfModule>
- <IfModule !mod_authz_core.c>
- # Apache 2.2
- Order deny,allow
- Allow from all
- </IfModule>
-</Location>
diff --git a/fedora-elections/images/border-left.png b/fedora-elections/images/border-left.png
deleted file mode 100644
index 6f256df..0000000
Binary files a/fedora-elections/images/border-left.png and /dev/null differ
diff --git a/fedora-elections/images/border-right.png b/fedora-elections/images/border-right.png
deleted file mode 100644
index 0e7365b..0000000
Binary files a/fedora-elections/images/border-right.png and /dev/null differ
diff --git a/fedora-elections/images/fedora-logo.png b/fedora-elections/images/fedora-logo.png
deleted file mode 100644
index ce95893..0000000
Binary files a/fedora-elections/images/fedora-logo.png and /dev/null differ
diff --git a/fedora-elections/images/line.png b/fedora-elections/images/line.png
deleted file mode 100644
index 3498cb9..0000000
Binary files a/fedora-elections/images/line.png and /dev/null differ
diff --git a/fedora-elections/models.py b/fedora-elections/models.py
deleted file mode 100644
index b19bd33..0000000
--- a/fedora-elections/models.py
+++ /dev/null
@@ -1,103 +0,0 @@
-from fedora_elections import app, db
-
-from datetime import datetime
-
-class Election(db.Model):
- __tablename__ = 'elections'
-
- id = db.Column(db.Integer, primary_key=True)
- summary = db.Column(db.Unicode(150), unique=True, nullable=False)
- alias = db.Column(db.Unicode(100), unique=True, nullable=False)
- description = db.Column(db.UnicodeText, nullable=False)
- url = db.Column(db.Unicode(250), nullable=False)
- start_date = db.Column(db.DateTime, nullable=False)
- end_date = db.Column(db.DateTime, nullable=False)
- number_elected = db.Column(db.Integer, nullable=False, default=1)
- embargoed = db.Column(db.Boolean, nullable=False, default=True)
- frontpage = db.Column(db.Boolean, nullable=False, default=False)
- voting_type = db.Column(db.Unicode(100), nullable=False, default=u'range')
- candidates_are_fasusers = db.Column(db.Boolean, nullable=False,
- default=False)
- fas_user = db.Column(db.Unicode(50), nullable=False)
-
- @property
- def status(self):
- if datetime.now() < self.start_date:
- return 'Pending'
- else:
- if datetime.now() < self.end_date:
- return 'In progress'
- else:
- if self.embargoed:
- return 'Embargoed'
- else:
- return 'Ended'
-
- @property
- def locked(self):
- return datetime.now() >= self.start_date
-
-
-class ElectionAdminGroup(db.Model):
- __tablename__ = 'electionadmingroups'
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('admin_groups',
- lazy='dynamic'))
- group_name = db.Column(db.Unicode(150), nullable=False)
- role_required = db.Column(db.Enum(u'user', u'sponsor', u'administrator'),
- nullable=False)
-
-
-class Candidate(db.Model):
- __tablename__ = 'candidates'
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('candidates',
- lazy='dynamic'))
- name = db.Column(db.Unicode(150), nullable=False) # FAS username if
- # candidates_are_fasusers
- url = db.Column(db.Unicode(250))
-
- @property
- def vote_count(self):
- values = [v.value for v in self.votes]
- return sum(values)
-
-
-class LegalVoter(db.Model):
- __tablename__ = 'legalvoters'
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('legal_voters',
- lazy='dynamic'))
- # special names:
- # "cla + one" = cla_done + 1 non-cla group
- group_name = db.Column(db.Unicode(150), nullable=False)
-
-
-class Vote(db.Model):
- __tablename__ = 'votes'
-
- __table_args__ = (db.UniqueConstraint('election_id', 'voter',
- 'candidate_id',
- name='eid_voter_cid'), {} )
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('votes',
- lazy='dynamic'))
- voter = db.Column(db.Unicode(150), nullable=False)
- timestamp = db.Column(db.DateTime, nullable=False)
- candidate_id = db.Column(db.Integer, db.ForeignKey('candidates.id'),
- nullable=False)
- candidate = db.relationship('Candidate', backref=db.backref('votes',
- lazy='dynamic'))
- value = db.Column(db.Integer, nullable=False)
diff --git a/fedora-elections/redirect.py b/fedora-elections/redirect.py
deleted file mode 100644
index 5571abe..0000000
--- a/fedora-elections/redirect.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import flask
-
-from urlparse import urljoin, urlparse
-
-
-def is_safe_url(target):
- ref_url = urlparse(flask.request.host_url)
- test_url = urlparse(urljoin(flask.request.host_url, target))
- return test_url.scheme in ('http', 'https') and \
- ref_url.netloc == test_url.netloc
-
-
-def safe_redirect_back(next=None, fallback=('index', {})):
- targets = []
- if next:
- targets.append(next)
- if 'next' in flask.request.args and \
- flask.request.args['next']:
- targets.append(flask.request.args['next'])
- targets.append(flask.url_for(fallback[0], **fallback[1]))
- for target in targets:
- if is_safe_url(target):
- return flask.redirect(target)
diff --git a/fedora-elections/static/style.css b/fedora-elections/static/style.css
deleted file mode 100644
index ee09d79..0000000
--- a/fedora-elections/static/style.css
+++ /dev/null
@@ -1,144 +0,0 @@
-body {
- font-family: sans-serif;
- font-size: 12pt;
- line-height: 1.35;
-}
-
-#flashes {
- background-color: #ffa;
-}
-
-nav {
- background-color: #eee;
- padding: 0.5em 1em;
-}
-
-nav h2 {
- display: inline;
- font-size: inherit;
- margin-right: 1em;
-}
-
-nav ul {
- display: inline;
- list-style: none;
- padding: 0;
-}
-
-nav li {
- display: inline;
- margin-right: .5em;
-}
-
-.clear {
- clear: both;
-}
-
-/* input elements */
-
-fieldset {
- border: 0;
- padding: 0;
- margin: .5em 0;
- float: left;
- clear: both;
-}
-
-.row {
- margin: .25em 0;
-}
-
-.row label:first-child {
- width: 8em;
- margin-right: 0.75em;
- text-align: right;
- clear: both;
- font-weight: bold;
- display: inline-block;
- vertical-align: top;
-}
-
-.row textarea {
- width: 30em;
- height: 8em;
-}
-
-form .row .errors {
- list-style: none;
- margin: .5em 0 1em 10.9375em;
- padding: 0;
- font-size: .8em;
- color: #c00;
- width: 30em;
-}
-
-div.help {
- width: 40%;
- float: right;
- background-color: #ffa;
- border-top: 1px solid #ee9;
- padding: 1em;
-/* font-size: .8em; */
- margin: .625em 0;
-}
-
-div > *:first-child { margin-top: 0; }
-div > *:last-child { margin-bottom: 0; }
-
-form .buttons.indent {
- margin-left: 8.75em;
-}
-
-.vthot
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #FFFFFF;
- background: #9C0000;
-}
-
-.vtwarm
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #FFA300;
-}
-
-.vtmedwarm
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #93FF63;
-}
-
-.vtmedcool
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #00DAFE;
-}
-
-.vtcool
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #0033FF;
-}
-
-.vtcold
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #FFFFFF;
- background: #000083;
-}
diff --git a/fedora-elections/templates/_formhelpers.html b/fedora-elections/templates/_formhelpers.html
deleted file mode 100644
index ff781b7..0000000
--- a/fedora-elections/templates/_formhelpers.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% macro render_field(field, after="") %}
-<div class="row">
- {{ field.label }}{{ field(**kwargs)|safe }}{% if after %} {{ after }}{% endif %}
- {% if field.errors %}
- <ul class="errors">
- {% for error in field.errors %}
- <li>{{ error }}</li>
- {% endfor %}
- </ul>
- {% endif %}
-</div>
-{% endmacro %}
-{# vim: set ts=2 sw=2 sta et: #}
diff --git a/fedora-elections/templates/admin/all_elections.html b/fedora-elections/templates/admin/all_elections.html
deleted file mode 100644
index e168ac0..0000000
--- a/fedora-elections/templates/admin/all_elections.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% extends "base.html" %}
-{% block title %}Election administration{% endblock %}
-{% block body %}
- {% if num_elections > 0 %}
- <h2>Election administration</h2>
- <table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>
- <a href="{{ url_for('admin_view_election',
- election_alias=election.alias) }}">
- {{ election.summary }}
- </a>
- </td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- </tr>
- {% endfor %}
- </table>
- {% endif %}
-{%endblock %}
diff --git a/fedora-elections/templates/admin/candidate.html b/fedora-elections/templates/admin/candidate.html
deleted file mode 100644
index a7b2fda..0000000
--- a/fedora-elections/templates/admin/candidate.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}{{ submit_text }}{% endblock %}
-{% block body %}
- <h2>{{ submit_text }}</h2>
-
- <form action="" method="post">
- <fieldset>
- {{ render_field(form.name) }}
- {{ render_field(form.url) }}
- </fieldset>
-
- <br class="clear">
- <p class="buttons">
- <input type="submit" class="submit positive" value="{{ submit_text }}">
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora-elections/templates/admin/delete_candidate.html b/fedora-elections/templates/admin/delete_candidate.html
deleted file mode 100644
index d764f19..0000000
--- a/fedora-elections/templates/admin/delete_candidate.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}Delete candidate{% endblock %}
-{% block body %}
- <h2>{{ self.title() }}</h2>
-
- <form action="" method="post">
- <p>Are you sure you want to delete candidate "{{ candidate.name }}"?</p>
- <p class="buttons">
- <input type="submit" value="Delete candidate">
- <a href="{{ url_for('admin_view_election',
- election_alias=candidate.election.alias) }}">
- Cancel
- </a>
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora-elections/templates/admin/election_form.html b/fedora-elections/templates/admin/election_form.html
deleted file mode 100644
index 6e2a3d6..0000000
--- a/fedora-elections/templates/admin/election_form.html
+++ /dev/null
@@ -1,77 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}{{ submit_text }}{% endblock %}
-{% block body %}
- <h2>{{ self.title() }}</h2>
-
- <form action="" method="post">
- <fieldset>
- {{ render_field(form.summary) }}
- </fieldset>
- <div class="help">
- <p>This is the name of the election that users will first see.</p>
- <p>Example: <kbd>Fedora Board, November 2010</kbd></p>
- </div>
- <fieldset>
- {{ render_field(form.alias) }}
- </fieldset>
- <div class="help">
- <p>
- A short, unique name that will be used for the election's URL.
- Lowercase letters, numbers, hyphens and underscores only.
- </p>
- <p>Example: <kbd>boardf15</kbd></p>
- </div>
- <fieldset>
- {{ render_field(form.description) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.url) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.start_date, " at 00:00:00 UTC") }}
- {{ render_field(form.end_date, " at 23:59:59 UTC") }}
- </fieldset>
- <div class="help">
- <p>Enter dates in <kbd>YYYY-MM-DD</kbd> format.</p>
- </div>
- <fieldset>
- {{ render_field(form.number_elected) }}
- </fieldset>
- <div class="help">
- <p>
- This determines the cutoff for how many candidates win the election.
- For example, if you are electing four empty seats on a committee,
- enter <kbd>4</kbd> here.
- </p>
- </div>
- <fieldset>
- {{ render_field(form.candidates_are_fasusers) }}
- </fieldset>
- <div class="help">
- <p>
- Check this if every candidate in this election has a FAS user account.
- This will automatically pull full names and wiki page URLs from FAS.
- </p>
- </div>
- <fieldset>
- {{ render_field(form.frontpage) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.embargoed) }}
- </fieldset>
- <div class="help">
- <p>
- Embargo results when the election is finished. To unembargo them, edit
- the election and uncheck this box.
- </p>
- </div>
-
- <br class="clear">
- {% block election_form_extra %}{% endblock %}
- <p class="buttons">
- <input type="submit" class="submit positive" value="Save Changes">
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora-elections/templates/admin/view_election.html b/fedora-elections/templates/admin/view_election.html
deleted file mode 100644
index ed070f3..0000000
--- a/fedora-elections/templates/admin/view_election.html
+++ /dev/null
@@ -1,42 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- <h2>{{ election.summary }}</h2>
- <p>{{ election.description }}</p>
- <p>
- <a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">
- Edit election
- </a>
- </p>
- <ul>
- <li>On front page: {{ election.frontpage }}</li>
- <li>Start date: {{ election.start_date }}</li>
- <li>End date: {{ election.end_date }}</li>
- <li>Number elected: {{ election.number_elected }}</li>
- <li>Election type: {{ election.voting_type }}</li>
- </ul>
- <h3>Candidates</h3>
- {% if election.candidates.all() %}
- <ul>
- {% for candidate in election.candidates.all() %}
- <li>{{ candidate.name }}
- <a href="{{ url_for('admin_edit_candidate',
- election_alias=election.alias,
- candidate_id=candidate.id) }}">
- [edit]
- </a>
- <a href="{{ url_for('admin_delete_candidate',
- election_alias=election.alias,
- candidate_id=candidate.id) }}">
- [x]
- </a>
- </li>
- {% endfor %}
- </ul>
- {% else %}
- <p>There are no candidates.</p>
- {% endif %}
- <a href="{{ url_for('admin_add_candidate',
- election_alias=election.alias) }}">
- Add a candidate
- </a>
-{% endblock %}
diff --git a/fedora-elections/templates/auth/login.html b/fedora-elections/templates/auth/login.html
deleted file mode 100644
index 9e7b73c..0000000
--- a/fedora-elections/templates/auth/login.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}Log in{% endblock %}
-{% block body %}
- <h2>Log in</h2>
-
- <form action="" method="post">
- <p>
- Log in using your
- <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a>
- account.
- </p>
- {{ render_field(form.username) }}
- {{ render_field(form.password) }}
- <p class="buttons indent"><input type="submit" class="submit positive"
- value="Log in">
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora-elections/templates/base.html b/fedora-elections/templates/base.html
deleted file mode 100644
index 3e6f806..0000000
--- a/fedora-elections/templates/base.html
+++ /dev/null
@@ -1,134 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- {% block head %}
- <title>
- Fedora Elections
- {% if self.title() %}
- —
- {% block title %}{% endblock %}
- {% endif %}
- </title>
- <link rel="stylesheet" type="text/css" media="screen"
- href="{{ url_for("static", filename="style.css") }}" />
- <link rel="stylesheet" type="text/css" media="screen"
- href="https://fedoraproject.org/static/css/fedora.css" />
- {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
- {% endfor %}
- {% endblock %}
- </head>
- <body>
- <div id="wrapper">
- <div id="head">
- <h1><a href="https://admin.fedoraproject.org/en/index">Fedora</a></h1>
- </div>
-
- <div id="sidebar">
- <div id="nav">
- <h2>Navigation</h2>
- <ul>
- <li id="nav-home">
- <a href="http://fedoraproject.org/en/index">Home</a>
- </li>
- <li id="nav-get">
- <a href="http://fedoraproject.org/en/get-fedora">Get Fedora</a>
- </li>
- <li id="nav-join">
- <a href="http://fedoraproject.org/en/join-fedora">Join Fedora</a>
- </li>
- <li id="nav-help">
- <a href="http://fedoraproject.org/en/get-help">Get Help</a>
- </li>
- </ul>
- </div>
- </div>
-
- <div id="content">
- <ul class="toolbar" id="#main-toolbar">
- {% if g.fas_user %}
- <li>
- Hello, {{ g.fas_user.username }} —
- <a href="{{ url_for("auth_logout") }}">log out</a>
- </li>
- {% else %}
- <li>
- You are not logged in —
- <a href="{{ url_for("auth_login") }}">log in</a>
- </li>
- {% endif %}
- </ul>
- <h2>Elections</h2>
- <ul>
- <li>
- <a href="{{ url_for("open_elections") }}">
- Open elections
- </a>
- </li>
- <li>
- <a href="{{ url_for("archived_elections") }}">
- Archived elections
- </a>
- </li>
- </ul>
- <br>
- <h2>Administration</h2>
- <ul>
- <li>
- <a href="{{ url_for("admin_view_elections") }}">
- My elections
- </a>
- </li>
- <li>
- <a href="{{ url_for("admin_new_election") }}">
- Create new election
- </a>
- </li>
- </ul>
- {% with messages = get_flashed_messages() %}
- {% if messages %}
- <ul id="flashes">
- {% for message in messages %}
- <li>{{ message }}</li>
- {% endfor %}
- </ul>
- {% endif %}
- {% endwith %}
- {% block body %}
- {% endblock %}
- </div>
- </div>
- <div id="bottom">
- <div id="footer">
- {% block footer %}
- <p class="copy">
- Copyright © 2008 Red Hat, Inc. and others. Please send any
- comments or corrections to the
- <a href="mailto:webmaster@fedoraproject.org">
- websites team
- </a>.
- </p>
- <p class="disclaimer">
- The Fedora Project is maintained and driven by the community and
- sponsored by Red Hat. This is a community maintained site. Red
- Hat is not responsible for content.
- </p>
- <ul>
- <li class="first">
- <a href="/en/sponsors">Sponsors</a>
- </li>
- <li>
- <a href="http://fedoraproject.org/wiki/Legal">
- Legal
- </a>
- </li>
- <li>
- <a href="http://fedoraproject.org/wiki/Legal/TrademarkGuidelines">
- Trademark Guidelines
- </a>
- </li>
- </ul>
- </div>
- {% endblock %}
- </div>
- </body>
-</html>
diff --git a/fedora-elections/templates/election/about.html b/fedora-elections/templates/election/about.html
deleted file mode 100644
index c7bf0ec..0000000
--- a/fedora-elections/templates/election/about.html
+++ /dev/null
@@ -1,40 +0,0 @@
-{% extends "base.html" %}
-{% block title %}Election Information{% endblock %}
-{% block body %}
- <h1>{{ election.summary }}</h1>
- <p>{{ election.description }}</p>
- {% if election.url %}
- <p><a href="{{ election.url }}">[More Information]</a></p>
- {% endif %}
- {% if election.status == 'Ended' %}
- <a href="/results/{{ election.alias }}">Show Results!</a>
- {% endif %}
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- {% if candidate.url %}
- <small><a href="{{ candidate.url }}">[info]</a></small>
- {% endif %}
- </td>
- </tr>
- {% endfor %}
- </table>
- {% if election.legal_voters.count() != 0 %}
- <p>
- To vote in this election you must be a member of any one of the following groups:
- </p>
- <ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
- </ul>
- {% else %}
- <p />
- {% endif %}
-{% endblock %}
diff --git a/fedora-elections/templates/election/archive.html b/fedora-elections/templates/election/archive.html
deleted file mode 100644
index 8740b3c..0000000
--- a/fedora-elections/templates/election/archive.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- {% if num_elections > 0 %}
- <h2>Past Elections</h2>
- <table border="1" cellpadding="1">
- <tr>
- <th></th>
- <th>Start Date (UTC)</th>
- <th>End Date (UTC)</th>
- <th></th>
- <th></th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>{{election.description}}</td>
- <td>{{election.start_date}}</td>
- <td>{{election.end_date}}</td>
- <td>
- <a href="about/{{ election.alias }}"">More information</a>
- </td>
- <td>
- <a href="results/{{ election.alias }}">Results</a>
- </td>
- </tr>
- {% endfor %}
- </table>
- {% endif %}
-{% endblock %}
diff --git a/fedora-elections/templates/election/results.html b/fedora-elections/templates/election/results.html
deleted file mode 100644
index f61d214..0000000
--- a/fedora-elections/templates/election/results.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- <h1>{{ election.summary }} Results</h1>
- <p>{{ election.description }}</p>
- {% if election.url %}
- <p><a href="{{ election.url }}">[More Information]</a></p>
- {% endif %}
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{candidate.name}}
- {% endif %}
- {% if candidate.url %}
- <small><a href="{{ candidate.url }}">[info]</a></small>
- {% endif %}
- </td>
- <td>{{ candidate.vote_count }}</td>
- </tr>
- {% endfor %}
- </table>
-{% endblock %}
diff --git a/fedora-elections/templates/election/vote.html b/fedora-elections/templates/election/vote.html
deleted file mode 100644
index e3c4dd4..0000000
--- a/fedora-elections/templates/election/vote.html
+++ /dev/null
@@ -1,116 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-<h2>{{election.summary}}</h2>
-
-<p>{{election.description}}</p>
-
-<p><a href="{{election.url}}">[More Information]</a></p>
-
-<form action="" method="post">
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- <a href="{{candidate.url}}">[Info]</a>
- </td>
- <!-- If user is logged in: -->
- {% if g.fas_user %}
- {% if nextaction == 'vote' %}
- <td>
- <select name="{{candidate.id}}">
- {% for opt in range(0, num_candidates+1) %}
- <option value={{opt}}>{{opt}}</option>
- {% endfor %}
- </select>
- </td>
- {% endif %}
- {% if nextaction == 'confirm' %}
- <td>
- {% set cid = candidate.id %}
- <input type="hidden" name="{{candidate.id}}"
- value="{{voteinfo[cid]}}" />
- {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
- {% if weightpercent >= 90 %}
- <div class="vthot">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 70 %}
- <div class="vtwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 50 %}
- <div class="vtmedwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 30 %}
- <div class="vtmedcool">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 10 %}
- <div class="vtcool">
- {{voteinfo[cid]}}
- </div>
- {% else %}
- <div class="vtcold">
- {{voteinfo[cid]}}
- </div>
- {% endif %}
- </td>
- {% endif %}
- {% endif %}
- <!-- End if user logged in -->
- </tr>
- {% endfor %}
- </table>
- {% if g.fas_user %}
- <p>
- {% if nextaction == 'confirm' %}
- <input type="submit" name="{{nextaction}}" value="Submit" />
- {% else %}
- <input type="submit" name="{{nextaction}}" value="Preview" />
- {% endif %}
- </p>
- {% endif %}
-</form>
-
-<!-- Make this conditional on whether the user is logged in and can vote in the election -->
-{% if election.legal_voters.count() != 0 %}
-<p>
- To vote in this election you must be a member of any one of the following groups:
-</p>
-<ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
-</ul>
-{% else %}
-<p />
-{% endif %}
-<div>
- <h2>Information on Range Voting</h2>
- <p>
- Fedora Project has implemented
- <a href="http://rangevoting.org/">Range Voting</a>
- for this election, in particular the "Range (score-summing, blanks
- treated as zero score, no quorum rule)" range voting system.
- </p>
- <p>
- To cast your vote in this election simply select a value between 0 and
- {{num_candidates}} with 0 as 'least or no preference' and
- {{num_candidates}} as 'highest preference'.
- </p>
- <p>
- At the end of the election, the highest ranking candidate(s) are marked
- as the winners.
- </p>
- <p>
- For more information about Range Voting, visit the
- <a href="http://rangevoting.org/">Center for Range Voting</a>.
- </p>
-</div>
-{% endblock %}
diff --git a/fedora-elections/templates/list/index.html b/fedora-elections/templates/list/index.html
deleted file mode 100644
index 290c2cf..0000000
--- a/fedora-elections/templates/list/index.html
+++ /dev/null
@@ -1,32 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- {% if num_elections > 0 %}
- <h2>{{ title }}</h2>
- <table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- <th></th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>{{ election.summary }}</td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- <td>
- {% if election.status == "In progress" %}
- {% if g.fas_user %}
- <a href="{{ url_for('vote', election_alias=election.alias) }}">
- Vote now!
- </a>
- {% endif %}
- {% endif %}
- </td>
- </tr>
- {% endfor %}
- </table>
- {% endif %}
-{% endblock %}
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
new file mode 100644
index 0000000..86bfdc6
--- /dev/null
+++ b/fedora_elections/__init__.py
@@ -0,0 +1,481 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2012 Red Hat, Inc.
+# Copyright © 2012 Ian Weller <ianweller(a)fedoraproject.org>
+# Copyright © 2012 Toshio Kuratomi <tkuratom(a)redhat.com>
+# Copyright © 2012 Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+#
+# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
+# Toshio Kuratomi <tkuratom(a)redhat.com>
+# Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+
+import flask
+from flask.ext.fas import FAS
+from flask.ext.sqlalchemy import SQLAlchemy
+
+from fedora.client import AuthError, AppError
+from fedora.client.fas2 import AccountSystem
+
+from sqlalchemy.orm.exc import NoResultFound
+
+import fedmsgshim
+
+from datetime import datetime, time
+from functools import wraps
+from urlparse import urlparse, urljoin
+
+app = flask.Flask(__name__)
+app.config.from_object('fedora_elections.default_config')
+app.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
+
+# set up SQLAlchemy
+db = SQLAlchemy(app)
+
+# set up FAS
+fas = FAS(app)
+
+# FAS for usernames.
+# baseURL = "https://admin.fedoraproject.org/accounts"
+# fasuser = "elections"
+# faspw = "elections"
+# fas2_url = 'http://fakefas.fedoraproject.org/accounts/'
+fas2_url = app.config['FAS_BASE_URL']
+fas2_username = app.config['FAS_USERNAME']
+fas2_password = app.config['FAS_PASSWORD']
+fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password,
+ insecure=True)
+
+
+# modular imports
+from fedora_elections import forms, models, redirect
+
+
+def remove_csrf(form_data):
+ return dict([(k, v) for k, v in form_data.items() if k != 'csrf'])
+
+
+def login_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if flask.g.fas_user is None:
+ return flask.redirect(flask.url_for('auth_login',
+ next=flask.request.url))
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+def election_admin_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if flask.g.fas_user is None:
+ return flask.redirect(flask.url_for('auth_login',
+ next=flask.request.url))
+ if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
+ flask.g.fas_user.groups:
+ flask.abort(403)
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+### LIST VIEWS #############################################
+
+(a)app.route('/')
+def index():
+ elections = models.Election.query.filter_by(frontpage=True).all()
+ num_elections = len(elections)
+ return flask.render_template('list/index.html', elections=elections,
+ num_elections=num_elections,
+ title="Elections")
+
+
+(a)app.route('/about/<election_alias>')
+def about_election(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/about.html', election=election,
+ usernamemap=usernamemap)
+
+
+(a)app.route('/archive')
+def archived_elections():
+ now = datetime.utcnow()
+
+ try:
+ elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date<now, models.Election.id>0).all()
+ except NoResultFound:
+ flask.flash('There are no archived elections.')
+ return redirect.safe_redirect_back()
+
+ num_elections = len(elections)
+ if (num_elections == 0):
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('election/archive.html',
+ num_elections=num_elections,
+ elections=elections)
+
+
+(a)app.route('/open')
+def open_elections():
+ now = datetime.utcnow()
+
+ try:
+ elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date>now,
+ models.Election.id>0).all()
+ except NoResultFound:
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ num_elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date>now,
+ models.Election.id>0).count()
+ if (num_elections == 0):
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('list/index.html', elections=elections,
+ num_elections=num_elections,
+ title='Open Elections')
+
+
+### ELECTION VIEWS #############################################
+
+(a)app.route('/vote/<election_alias>', methods=['GET', 'POST'])
+def vote(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'Pending':
+ flask.flash('Voting has not yet started, sorry.')
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Ended':
+ flask.flash('This election is closed. You have been redirected to ' +
+ 'the election results.')
+ return flask.redirect(flask.url_for('results', election.shortname))
+
+ votes = models.Vote.query.filter_by(election_id=election.id,
+ voter=flask.g.fas_user.username).count()
+ if (votes != 0):
+ flask.flash('You have already voted in the election!')
+ return redirect.safe_redirect_back()
+
+ num_candidates = election.candidates.count()
+
+ uvotes = {}
+ for candidate in election.candidates:
+ uvotes[candidate.id] = ""
+ next_action = 'vote'
+
+ if flask.request.method == 'POST':
+ next_action = ''
+ form_values = flask.request.form.values()
+ if 'Submit' in form_values:
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote >= 0 and vote <= num_candidates:
+ uvotes[candidate.id] = int(vote)
+ else:
+ flask.flash("Invalid Ballot!")
+ return redirect.safe_redirect_back()
+
+ except ValueError:
+ flask.flash("Invalid Ballot!")
+ return redirect.safe_redirect_back()
+
+ for uvote in uvotes:
+ new_vote = models.Vote(election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=uvote,
+ value=uvotes[uvote] )
+ db.session.add(new_vote)
+ db.session.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return redirect.safe_redirect_back()
+
+ elif 'Preview' in form_values:
+ flask.flash("Please confirm your vote!")
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote > num_candidates:
+ flask.flash("Invalid data.")
+ uvotes[candidate.id] = 0
+ next_action = 'vote'
+ elif vote >= 0:
+ uvotes[candidate.id] = vote
+ else:
+ flask.flash("Invalid data2.")
+ uvotes[c.id] = 0
+ next_action = 'vote'
+ except ValueError:
+ flask.flash("Invalid data")
+
+ if (next_action != 'vote'):
+ next_action = 'confirm'
+
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/vote.html', election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ voteinfo=uvotes, nextaction=next_action)
+
+
+### AUTH VIEWS #############################################
+
+(a)app.route('/login', methods=('GET', 'POST'))
+def auth_login():
+ if flask.g.fas_user:
+ return redirect.safe_redirect_back()
+ form = forms.LoginForm()
+ if form.validate_on_submit():
+ if fas.login(form.username.data, form.password.data):
+ flask.flash('Welcome, %s' % flask.g.fas_user.username)
+ return redirect.safe_redirect_back()
+ else:
+ flask.flash('Incorrect username or password')
+ return flask.render_template('auth/login.html', form=form)
+
+
+(a)app.route('/logout')
+def auth_logout():
+ if not flask.g.fas_user:
+ return redirect.safe_redirect_back()
+ fas.logout()
+ flask.flash('You have been logged out')
+ return redirect.safe_redirect_back()
+
+
+### ELECTION ADMIN VIEWS ###################################
+
+(a)app.route('/admin/')
+@election_admin_required
+def admin_view_elections():
+ elections = models.Election.query.\
+ filter_by(fas_user=flask.g.fas_user.username).all()
+ num_elections = len(elections)
+ if (num_elections == 0):
+ flask.flash('You do not have any elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('admin/all_elections.html',
+ num_elections=num_elections,
+ elections=elections)
+
+
+(a)app.route('/admin/new', methods=("GET", "POST"))
+@election_admin_required
+def admin_new_election():
+ form = forms.ElectionForm()
+ if form.validate_on_submit():
+ election = models.Election(**remove_csrf(form.data))
+ # Fix start_date and end_date to use datetime
+ election.start_date = datetime.combine(election.start_date, time())
+ election.end_date = datetime.combine(election.end_date,
+ time(23, 59, 59))
+ election.fas_user = flask.g.fas_user.username
+ db.session.add(election)
+ admin = models.ElectionAdminGroup(election=election,
+ group_name=app.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ role_required=u'user')
+ db.session.add(admin)
+ db.session.commit()
+ flask.flash('Election "%s" added' % election.summary)
+ fedmsgshim.publish(topic="election.new", msg=election)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/election_form.html', form=form,
+ submit_text='Create election')
+
+
+(a)app.route('/admin/<election_alias>/')
+@election_admin_required
+def admin_view_election(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ return flask.render_template('admin/view_election.html', election=election)
+
+
+(a)app.route('/admin/<election_alias>/edit', methods=("GET", "POST"))
+@election_admin_required
+def admin_edit_election(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ form = forms.ElectionForm(election.id, obj=election)
+ if form.validate_on_submit():
+ form.populate_obj(election)
+ db.session.commit()
+ flask.flash('Election "%s" saved' % election.summary)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/election_form.html', form=form,
+ submit_text='Edit election')
+
+
+(a)app.route('/admin/<election_alias>/candidates/new', methods=("GET", "POST"))
+@election_admin_required
+def admin_add_candidate(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ form = forms.CandidateForm()
+ if form.validate_on_submit():
+ candidate = models.Candidate(election=election,
+ **remove_csrf(form.data))
+ db.session.add(candidate)
+ db.session.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/candidate.html', form=form,
+ submit_text='Add candidate')
+
+
+(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
+ methods=("GET", "POST"))
+@election_admin_required
+def admin_edit_candidate(election_alias, candidate_id):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ if not election or not candidate:
+ flask.abort(404)
+ if candidate.election != election:
+ flask.abort(404)
+ form = forms.CandidateForm(obj=candidate)
+ if form.validate_on_submit():
+ form.populate_obj(candidate)
+ db.session.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/candidate.html', form=form,
+ submit_text='Edit candidate')
+
+
+(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
+ methods=("GET", "POST"))
+@election_admin_required
+def admin_delete_candidate(election_alias, candidate_id):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ if not election or not candidate:
+ flask.abort(404)
+ if candidate.election != election:
+ flask.abort(404)
+ form = forms.ConfirmationForm()
+ if form.validate_on_submit():
+ db.session.delete(candidate)
+ candidate_name = candidate.name
+ db.session.commit()
+ flask.flash('Candidate "%s" deleted' % candidate_name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/delete_candidate.html', form=form,
+ candidate=candidate)
+
+
+(a)app.route('/results/<election_alias>')
+def election_results(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'In progress':
+ flask.flash("We are sorry. The results for this election cannot be" \
+ " viewed at this time because the election is still in" \
+ " progress.")
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Pending':
+ flask.flash("We are sorry. The results for this election cannot be" \
+ " viewed at this time because the election has not" \
+ " started.")
+ return redirect.safe_redirect_back()
+
+ elif election.embargoed == 1:
+ if flask.g.fas_user is None:
+ flask.flash("We are sorry. The results for this election" \
+ "cannot be viewed because they are currently" \
+ " embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
+ else:
+ if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
+ flask.g.fas_user.groups:
+ pass
+ else:
+ match = 0
+ admingroups = models.ElectionAdminGroup.query.filter_by(
+ election_id=election.id).all()
+ for admingroup in admingroups:
+ if admingroup.group_name in flask.g.fas_user.groups:
+ match = 1
+
+ if match == 0:
+ flask.flash("We are sorry. The results for this election" \
+ "cannot be viewed because they are currently" \
+ " embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/results.html', election=election,
+ usernamemap=usernamemap)
diff --git a/fedora_elections/createdb.in b/fedora_elections/createdb.in
new file mode 100644
index 0000000..e921d8a
--- /dev/null
+++ b/fedora_elections/createdb.in
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora_elections/fedora-elections.cfg python @DATADIR(a)/fedora_elections/createdb.py
diff --git a/fedora_elections/createdb.py b/fedora_elections/createdb.py
new file mode 100644
index 0000000..a909c5e
--- /dev/null
+++ b/fedora_elections/createdb.py
@@ -0,0 +1,6 @@
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7']
+import pkg_resources
+
+from fedora_elections import db
+db.create_all()
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
new file mode 100644
index 0000000..7b5b794
--- /dev/null
+++ b/fedora_elections/default_config.py
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
diff --git a/fedora_elections/fedmsgshim.py b/fedora_elections/fedmsgshim.py
new file mode 100644
index 0000000..775f785
--- /dev/null
+++ b/fedora_elections/fedmsgshim.py
@@ -0,0 +1,17 @@
+""" This is a temporary shim that allows fedmsg to be an optional dependency of
+elections. If fedmsg is installed, these function calls will try to actually send
+messages. If it is not installed, it will return silently.
+
+ :Author: Ralph Bean <rbean(a)redhat.com>
+
+"""
+
+import warnings
+
+
+def publish(*args, **kwargs):
+ try:
+ import fedmsg
+ fedmsg.publish(*args, **kwargs)
+ except Exception, e:
+ warnings.warn(str(e))
diff --git a/fedora_elections/fedora-elections.cfg.in b/fedora_elections/fedora-elections.cfg.in
new file mode 100644
index 0000000..4b49118
--- /dev/null
+++ b/fedora_elections/fedora-elections.cfg.in
@@ -0,0 +1,18 @@
+import os, os.path
+
+DEBUG = True
+
+
+# You will want to change this for your install
+SECRET_KEY = 'change me'
+SQLALCHEMY_DATABASE_URI = 'sqlite:///@SHAREDSTDIR@/fedora_elections/database.sqlite'
+
+FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
+FAS_USERNAME = 'fcelections'
+FAS_PASSWORD = 'el3cTo$hozer'
+FAS_CHECK_CERT = False
+FAS_FLASK_COOKIE_REQUIRES_HTTPS = False
+
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+# List of URLs for additional site-wide CSS
+EXTRA_CSS = []
diff --git a/fedora_elections/fedora-elections.wsgi.in b/fedora_elections/fedora-elections.wsgi.in
new file mode 100644
index 0000000..ecdccc9
--- /dev/null
+++ b/fedora_elections/fedora-elections.wsgi.in
@@ -0,0 +1,21 @@
+#-*- coding: UTF-8 -*-
+
+# These two lines are needed to run on EL6
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import os
+import sys
+
+sys.stdout = sys.stderr
+
+## Set the environment variable pointing to the configuration file
+os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora_elections/fedora-elections.cfg'
+
+## The following is only needed if you did not install
+## as a python module (for example if you run it from a git clone).
+import sys
+# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
+
+from fedora_elections import app as application
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
new file mode 100644
index 0000000..0f769a2
--- /dev/null
+++ b/fedora_elections/forms.py
@@ -0,0 +1,73 @@
+from flask.ext import wtf
+
+from fedora_elections.models import Election
+
+
+class ElectionForm(wtf.Form):
+ summary = wtf.TextField('Summary', [wtf.validators.Required(),
+ wtf.validators.Length(max=150)])
+ alias = wtf.TextField('Alias', [
+ wtf.validators.Required(),
+ wtf.validators.Length(max=100),
+ wtf.validators.Regexp('[a-z0-9_-]+',
+ message=('Alias may only contain lower case '
+ 'letters, numbers, hyphens and '
+ 'underscores.')),
+ ])
+ description = wtf.TextAreaField('Description', [wtf.validators.Required()])
+ voting_type = wtf.RadioField('Type',
+ choices=[('range', 'Range Voting'),
+ ('simple', 'Simple Voting')],
+ default='range')
+ url = wtf.TextField('URL', [wtf.validators.Required(),
+ wtf.validators.URL(),
+ wtf.validators.Length(max=250)])
+ start_date = wtf.DateField('Start date', [wtf.validators.Required()])
+ end_date = wtf.DateField('End date', [wtf.validators.Required()])
+ number_elected = wtf.IntegerField('Number elected',
+ [wtf.validators.Required(),
+ wtf.validators.NumberRange(min=1)],
+ default=1)
+ candidates_are_fasusers = wtf.BooleanField('Candidates are FAS users?')
+ frontpage = wtf.BooleanField('Show on front page?')
+ embargoed = wtf.BooleanField('Embargo results?', default=True)
+
+ def __init__(form, election_id=None, *args, **kwargs):
+ super(ElectionForm, form).__init__(*args, **kwargs)
+ form._election_id = election_id
+
+ def validate_summary(form, field):
+ check = Election.query.filter_by(summary=form.summary.data).all()
+ if check:
+ if not (form._election_id and form._election_id == check[0].id):
+ raise wtf.ValidationError('There is already another election '
+ 'with this summary.')
+
+ def validate_alias(form, field):
+ if form.alias.data == 'new':
+ raise wtf.ValidationError(flask.Markup('The alias cannot be '
+ '<code>new</code>.'))
+ check = Election.query.filter_by(alias=form.alias.data).all()
+ if check:
+ if not (form._election_id and form._election_id == check[0].id):
+ raise wtf.ValidationError('There is already another election '
+ 'with this alias.')
+
+ def validate_end_date(form, field):
+ if form.end_date.data <= form.start_date.data:
+ raise wtf.ValidationError('End date must be later than '
+ 'start date.')
+
+
+class CandidateForm(wtf.Form):
+ name = wtf.TextField('Name', [wtf.validators.Required()])
+ url = wtf.TextField('URL', [wtf.validators.Length(max=250)])
+
+
+class LoginForm(wtf.Form):
+ username = wtf.TextField('Username', [wtf.validators.Required()])
+ password = wtf.PasswordField('Password', [wtf.validators.Required()])
+
+
+class ConfirmationForm(wtf.Form):
+ pass
diff --git a/fedora_elections/httpd-fedora-elections.conf.in b/fedora_elections/httpd-fedora-elections.conf.in
new file mode 100644
index 0000000..c79d2d9
--- /dev/null
+++ b/fedora_elections/httpd-fedora-elections.conf.in
@@ -0,0 +1,23 @@
+Alias /fedora-elections/images @DATADIR@/fedora_elections/images
+Alias /fedora-elections/static @DATADIR@/fedora_elections/static
+
+WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
+WSGISocketPrefix run/wsgi
+WSGIRestrictStdout On
+WSGIRestrictSignal Off
+WSGIPythonOptimize 1
+
+WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora_elections/fedora-elections.wsgi
+
+<Location /fedora-elections>
+ WSGIProcessGroup fedora-elections
+ <IfModule mod_authz_core.c>
+ # Apache 2.4
+ Require all granted
+ </IfModule>
+ <IfModule !mod_authz_core.c>
+ # Apache 2.2
+ Order deny,allow
+ Allow from all
+ </IfModule>
+</Location>
diff --git a/fedora_elections/images/border-left.png b/fedora_elections/images/border-left.png
new file mode 100644
index 0000000..6f256df
Binary files /dev/null and b/fedora_elections/images/border-left.png differ
diff --git a/fedora_elections/images/border-right.png b/fedora_elections/images/border-right.png
new file mode 100644
index 0000000..0e7365b
Binary files /dev/null and b/fedora_elections/images/border-right.png differ
diff --git a/fedora_elections/images/fedora-logo.png b/fedora_elections/images/fedora-logo.png
new file mode 100644
index 0000000..ce95893
Binary files /dev/null and b/fedora_elections/images/fedora-logo.png differ
diff --git a/fedora_elections/images/line.png b/fedora_elections/images/line.png
new file mode 100644
index 0000000..3498cb9
Binary files /dev/null and b/fedora_elections/images/line.png differ
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
new file mode 100644
index 0000000..b19bd33
--- /dev/null
+++ b/fedora_elections/models.py
@@ -0,0 +1,103 @@
+from fedora_elections import app, db
+
+from datetime import datetime
+
+class Election(db.Model):
+ __tablename__ = 'elections'
+
+ id = db.Column(db.Integer, primary_key=True)
+ summary = db.Column(db.Unicode(150), unique=True, nullable=False)
+ alias = db.Column(db.Unicode(100), unique=True, nullable=False)
+ description = db.Column(db.UnicodeText, nullable=False)
+ url = db.Column(db.Unicode(250), nullable=False)
+ start_date = db.Column(db.DateTime, nullable=False)
+ end_date = db.Column(db.DateTime, nullable=False)
+ number_elected = db.Column(db.Integer, nullable=False, default=1)
+ embargoed = db.Column(db.Boolean, nullable=False, default=True)
+ frontpage = db.Column(db.Boolean, nullable=False, default=False)
+ voting_type = db.Column(db.Unicode(100), nullable=False, default=u'range')
+ candidates_are_fasusers = db.Column(db.Boolean, nullable=False,
+ default=False)
+ fas_user = db.Column(db.Unicode(50), nullable=False)
+
+ @property
+ def status(self):
+ if datetime.now() < self.start_date:
+ return 'Pending'
+ else:
+ if datetime.now() < self.end_date:
+ return 'In progress'
+ else:
+ if self.embargoed:
+ return 'Embargoed'
+ else:
+ return 'Ended'
+
+ @property
+ def locked(self):
+ return datetime.now() >= self.start_date
+
+
+class ElectionAdminGroup(db.Model):
+ __tablename__ = 'electionadmingroups'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('admin_groups',
+ lazy='dynamic'))
+ group_name = db.Column(db.Unicode(150), nullable=False)
+ role_required = db.Column(db.Enum(u'user', u'sponsor', u'administrator'),
+ nullable=False)
+
+
+class Candidate(db.Model):
+ __tablename__ = 'candidates'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('candidates',
+ lazy='dynamic'))
+ name = db.Column(db.Unicode(150), nullable=False) # FAS username if
+ # candidates_are_fasusers
+ url = db.Column(db.Unicode(250))
+
+ @property
+ def vote_count(self):
+ values = [v.value for v in self.votes]
+ return sum(values)
+
+
+class LegalVoter(db.Model):
+ __tablename__ = 'legalvoters'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('legal_voters',
+ lazy='dynamic'))
+ # special names:
+ # "cla + one" = cla_done + 1 non-cla group
+ group_name = db.Column(db.Unicode(150), nullable=False)
+
+
+class Vote(db.Model):
+ __tablename__ = 'votes'
+
+ __table_args__ = (db.UniqueConstraint('election_id', 'voter',
+ 'candidate_id',
+ name='eid_voter_cid'), {} )
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('votes',
+ lazy='dynamic'))
+ voter = db.Column(db.Unicode(150), nullable=False)
+ timestamp = db.Column(db.DateTime, nullable=False)
+ candidate_id = db.Column(db.Integer, db.ForeignKey('candidates.id'),
+ nullable=False)
+ candidate = db.relationship('Candidate', backref=db.backref('votes',
+ lazy='dynamic'))
+ value = db.Column(db.Integer, nullable=False)
diff --git a/fedora_elections/redirect.py b/fedora_elections/redirect.py
new file mode 100644
index 0000000..5571abe
--- /dev/null
+++ b/fedora_elections/redirect.py
@@ -0,0 +1,23 @@
+import flask
+
+from urlparse import urljoin, urlparse
+
+
+def is_safe_url(target):
+ ref_url = urlparse(flask.request.host_url)
+ test_url = urlparse(urljoin(flask.request.host_url, target))
+ return test_url.scheme in ('http', 'https') and \
+ ref_url.netloc == test_url.netloc
+
+
+def safe_redirect_back(next=None, fallback=('index', {})):
+ targets = []
+ if next:
+ targets.append(next)
+ if 'next' in flask.request.args and \
+ flask.request.args['next']:
+ targets.append(flask.request.args['next'])
+ targets.append(flask.url_for(fallback[0], **fallback[1]))
+ for target in targets:
+ if is_safe_url(target):
+ return flask.redirect(target)
diff --git a/fedora_elections/static/style.css b/fedora_elections/static/style.css
new file mode 100644
index 0000000..ee09d79
--- /dev/null
+++ b/fedora_elections/static/style.css
@@ -0,0 +1,144 @@
+body {
+ font-family: sans-serif;
+ font-size: 12pt;
+ line-height: 1.35;
+}
+
+#flashes {
+ background-color: #ffa;
+}
+
+nav {
+ background-color: #eee;
+ padding: 0.5em 1em;
+}
+
+nav h2 {
+ display: inline;
+ font-size: inherit;
+ margin-right: 1em;
+}
+
+nav ul {
+ display: inline;
+ list-style: none;
+ padding: 0;
+}
+
+nav li {
+ display: inline;
+ margin-right: .5em;
+}
+
+.clear {
+ clear: both;
+}
+
+/* input elements */
+
+fieldset {
+ border: 0;
+ padding: 0;
+ margin: .5em 0;
+ float: left;
+ clear: both;
+}
+
+.row {
+ margin: .25em 0;
+}
+
+.row label:first-child {
+ width: 8em;
+ margin-right: 0.75em;
+ text-align: right;
+ clear: both;
+ font-weight: bold;
+ display: inline-block;
+ vertical-align: top;
+}
+
+.row textarea {
+ width: 30em;
+ height: 8em;
+}
+
+form .row .errors {
+ list-style: none;
+ margin: .5em 0 1em 10.9375em;
+ padding: 0;
+ font-size: .8em;
+ color: #c00;
+ width: 30em;
+}
+
+div.help {
+ width: 40%;
+ float: right;
+ background-color: #ffa;
+ border-top: 1px solid #ee9;
+ padding: 1em;
+/* font-size: .8em; */
+ margin: .625em 0;
+}
+
+div > *:first-child { margin-top: 0; }
+div > *:last-child { margin-bottom: 0; }
+
+form .buttons.indent {
+ margin-left: 8.75em;
+}
+
+.vthot
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #9C0000;
+}
+
+.vtwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #FFA300;
+}
+
+.vtmedwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #93FF63;
+}
+
+.vtmedcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #00DAFE;
+}
+
+.vtcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #0033FF;
+}
+
+.vtcold
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #000083;
+}
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
new file mode 100644
index 0000000..ff781b7
--- /dev/null
+++ b/fedora_elections/templates/_formhelpers.html
@@ -0,0 +1,13 @@
+{% macro render_field(field, after="") %}
+<div class="row">
+ {{ field.label }}{{ field(**kwargs)|safe }}{% if after %} {{ after }}{% endif %}
+ {% if field.errors %}
+ <ul class="errors">
+ {% for error in field.errors %}
+ <li>{{ error }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+</div>
+{% endmacro %}
+{# vim: set ts=2 sw=2 sta et: #}
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
new file mode 100644
index 0000000..e168ac0
--- /dev/null
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -0,0 +1,28 @@
+{% extends "base.html" %}
+{% block title %}Election administration{% endblock %}
+{% block body %}
+ {% if num_elections > 0 %}
+ <h2>Election administration</h2>
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>
+ <a href="{{ url_for('admin_view_election',
+ election_alias=election.alias) }}">
+ {{ election.summary }}
+ </a>
+ </td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+{%endblock %}
diff --git a/fedora_elections/templates/admin/candidate.html b/fedora_elections/templates/admin/candidate.html
new file mode 100644
index 0000000..a7b2fda
--- /dev/null
+++ b/fedora_elections/templates/admin/candidate.html
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}{{ submit_text }}{% endblock %}
+{% block body %}
+ <h2>{{ submit_text }}</h2>
+
+ <form action="" method="post">
+ <fieldset>
+ {{ render_field(form.name) }}
+ {{ render_field(form.url) }}
+ </fieldset>
+
+ <br class="clear">
+ <p class="buttons">
+ <input type="submit" class="submit positive" value="{{ submit_text }}">
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora_elections/templates/admin/delete_candidate.html b/fedora_elections/templates/admin/delete_candidate.html
new file mode 100644
index 0000000..d764f19
--- /dev/null
+++ b/fedora_elections/templates/admin/delete_candidate.html
@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}Delete candidate{% endblock %}
+{% block body %}
+ <h2>{{ self.title() }}</h2>
+
+ <form action="" method="post">
+ <p>Are you sure you want to delete candidate "{{ candidate.name }}"?</p>
+ <p class="buttons">
+ <input type="submit" value="Delete candidate">
+ <a href="{{ url_for('admin_view_election',
+ election_alias=candidate.election.alias) }}">
+ Cancel
+ </a>
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
new file mode 100644
index 0000000..e46bb3a
--- /dev/null
+++ b/fedora_elections/templates/admin/election_form.html
@@ -0,0 +1,80 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}{{ submit_text }}{% endblock %}
+{% block body %}
+ <h2>{{ self.title() }}</h2>
+
+ <form action="" method="post">
+ <fieldset>
+ {{ render_field(form.summary) }}
+ </fieldset>
+ <div class="help">
+ This is the name of the election that users will first see.</br>
+ Example: <kbd>Fedora Board, November 2010</kbd>
+ </div>
+ <fieldset>
+ {{ render_field(form.alias) }}
+ </fieldset>
+ <div class="help">
+ A short, unique name that will be used for the election's URL.</br>
+ Lowercase letters, numbers, hyphens and underscores only.</br>
+ Example: <kbd>boardf15</kbd>
+ </div>
+ <fieldset>
+ {{ render_field(form.description) }}
+ </fieldset>
+ <fieldset>
+ <div class="row">
+ {{ form.voting_type.label }}
+ {% for subtype in form.voting_type %}
+ {{ subtype }} {{ subtype.label }}
+ {% endfor %}
+ </div>
+ </fieldset>
+ <div class="help">
+ Select the election type.
+ </div>
+ <fieldset>
+ {{ render_field(form.url) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.start_date, " at 00:00:00 UTC") }}
+ {{ render_field(form.end_date, " at 23:59:59 UTC") }}
+ </fieldset>
+ <div class="help">
+ Enter dates in <kbd>YYYY-MM-DD</kbd> format.
+ </div>
+ <fieldset>
+ {{ render_field(form.number_elected) }}
+ </fieldset>
+ <div class="help">
+ This determines the cutoff for how many candidates win the election.
+ For example, if you are electing four empty seats on a committee,
+ enter <kbd>4</kbd> here.
+ </div>
+ <fieldset>
+ {{ render_field(form.candidates_are_fasusers) }}
+ </fieldset>
+ <div class="help">
+ Check this if every candidate in this election has a FAS user account.
+ This will automatically pull full names and wiki page URLs from FAS.
+ </div>
+ <fieldset>
+ {{ render_field(form.frontpage) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.embargoed) }}
+ </fieldset>
+ <div class="help">
+ Embargo results when the election is finished. To unembargo them, edit
+ the election and uncheck this box.
+ </div>
+
+ <br class="clear">
+ {% block election_form_extra %}{% endblock %}
+ <p class="buttons">
+ <input type="submit" class="submit positive" value="Save Changes">
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
new file mode 100644
index 0000000..ed070f3
--- /dev/null
+++ b/fedora_elections/templates/admin/view_election.html
@@ -0,0 +1,42 @@
+{% extends "base.html" %}
+{% block body %}
+ <h2>{{ election.summary }}</h2>
+ <p>{{ election.description }}</p>
+ <p>
+ <a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">
+ Edit election
+ </a>
+ </p>
+ <ul>
+ <li>On front page: {{ election.frontpage }}</li>
+ <li>Start date: {{ election.start_date }}</li>
+ <li>End date: {{ election.end_date }}</li>
+ <li>Number elected: {{ election.number_elected }}</li>
+ <li>Election type: {{ election.voting_type }}</li>
+ </ul>
+ <h3>Candidates</h3>
+ {% if election.candidates.all() %}
+ <ul>
+ {% for candidate in election.candidates.all() %}
+ <li>{{ candidate.name }}
+ <a href="{{ url_for('admin_edit_candidate',
+ election_alias=election.alias,
+ candidate_id=candidate.id) }}">
+ [edit]
+ </a>
+ <a href="{{ url_for('admin_delete_candidate',
+ election_alias=election.alias,
+ candidate_id=candidate.id) }}">
+ [x]
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>There are no candidates.</p>
+ {% endif %}
+ <a href="{{ url_for('admin_add_candidate',
+ election_alias=election.alias) }}">
+ Add a candidate
+ </a>
+{% endblock %}
diff --git a/fedora_elections/templates/auth/login.html b/fedora_elections/templates/auth/login.html
new file mode 100644
index 0000000..9e7b73c
--- /dev/null
+++ b/fedora_elections/templates/auth/login.html
@@ -0,0 +1,20 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}Log in{% endblock %}
+{% block body %}
+ <h2>Log in</h2>
+
+ <form action="" method="post">
+ <p>
+ Log in using your
+ <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a>
+ account.
+ </p>
+ {{ render_field(form.username) }}
+ {{ render_field(form.password) }}
+ <p class="buttons indent"><input type="submit" class="submit positive"
+ value="Log in">
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
new file mode 100644
index 0000000..3e6f806
--- /dev/null
+++ b/fedora_elections/templates/base.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ {% block head %}
+ <title>
+ Fedora Elections
+ {% if self.title() %}
+ —
+ {% block title %}{% endblock %}
+ {% endif %}
+ </title>
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="{{ url_for("static", filename="style.css") }}" />
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="https://fedoraproject.org/static/css/fedora.css" />
+ {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
+ {% endfor %}
+ {% endblock %}
+ </head>
+ <body>
+ <div id="wrapper">
+ <div id="head">
+ <h1><a href="https://admin.fedoraproject.org/en/index">Fedora</a></h1>
+ </div>
+
+ <div id="sidebar">
+ <div id="nav">
+ <h2>Navigation</h2>
+ <ul>
+ <li id="nav-home">
+ <a href="http://fedoraproject.org/en/index">Home</a>
+ </li>
+ <li id="nav-get">
+ <a href="http://fedoraproject.org/en/get-fedora">Get Fedora</a>
+ </li>
+ <li id="nav-join">
+ <a href="http://fedoraproject.org/en/join-fedora">Join Fedora</a>
+ </li>
+ <li id="nav-help">
+ <a href="http://fedoraproject.org/en/get-help">Get Help</a>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <div id="content">
+ <ul class="toolbar" id="#main-toolbar">
+ {% if g.fas_user %}
+ <li>
+ Hello, {{ g.fas_user.username }} —
+ <a href="{{ url_for("auth_logout") }}">log out</a>
+ </li>
+ {% else %}
+ <li>
+ You are not logged in —
+ <a href="{{ url_for("auth_login") }}">log in</a>
+ </li>
+ {% endif %}
+ </ul>
+ <h2>Elections</h2>
+ <ul>
+ <li>
+ <a href="{{ url_for("open_elections") }}">
+ Open elections
+ </a>
+ </li>
+ <li>
+ <a href="{{ url_for("archived_elections") }}">
+ Archived elections
+ </a>
+ </li>
+ </ul>
+ <br>
+ <h2>Administration</h2>
+ <ul>
+ <li>
+ <a href="{{ url_for("admin_view_elections") }}">
+ My elections
+ </a>
+ </li>
+ <li>
+ <a href="{{ url_for("admin_new_election") }}">
+ Create new election
+ </a>
+ </li>
+ </ul>
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+ <ul id="flashes">
+ {% for message in messages %}
+ <li>{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {% endwith %}
+ {% block body %}
+ {% endblock %}
+ </div>
+ </div>
+ <div id="bottom">
+ <div id="footer">
+ {% block footer %}
+ <p class="copy">
+ Copyright © 2008 Red Hat, Inc. and others. Please send any
+ comments or corrections to the
+ <a href="mailto:webmaster@fedoraproject.org">
+ websites team
+ </a>.
+ </p>
+ <p class="disclaimer">
+ The Fedora Project is maintained and driven by the community and
+ sponsored by Red Hat. This is a community maintained site. Red
+ Hat is not responsible for content.
+ </p>
+ <ul>
+ <li class="first">
+ <a href="/en/sponsors">Sponsors</a>
+ </li>
+ <li>
+ <a href="http://fedoraproject.org/wiki/Legal">
+ Legal
+ </a>
+ </li>
+ <li>
+ <a href="http://fedoraproject.org/wiki/Legal/TrademarkGuidelines">
+ Trademark Guidelines
+ </a>
+ </li>
+ </ul>
+ </div>
+ {% endblock %}
+ </div>
+ </body>
+</html>
diff --git a/fedora_elections/templates/election/about.html b/fedora_elections/templates/election/about.html
new file mode 100644
index 0000000..c7bf0ec
--- /dev/null
+++ b/fedora_elections/templates/election/about.html
@@ -0,0 +1,40 @@
+{% extends "base.html" %}
+{% block title %}Election Information{% endblock %}
+{% block body %}
+ <h1>{{ election.summary }}</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ {% if election.status == 'Ended' %}
+ <a href="/results/{{ election.alias }}">Show Results!</a>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p />
+ {% endif %}
+{% endblock %}
diff --git a/fedora_elections/templates/election/archive.html b/fedora_elections/templates/election/archive.html
new file mode 100644
index 0000000..8740b3c
--- /dev/null
+++ b/fedora_elections/templates/election/archive.html
@@ -0,0 +1,28 @@
+{% extends "base.html" %}
+{% block body %}
+ {% if num_elections > 0 %}
+ <h2>Past Elections</h2>
+ <table border="1" cellpadding="1">
+ <tr>
+ <th></th>
+ <th>Start Date (UTC)</th>
+ <th>End Date (UTC)</th>
+ <th></th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{election.description}}</td>
+ <td>{{election.start_date}}</td>
+ <td>{{election.end_date}}</td>
+ <td>
+ <a href="about/{{ election.alias }}"">More information</a>
+ </td>
+ <td>
+ <a href="results/{{ election.alias }}">Results</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+{% endblock %}
diff --git a/fedora_elections/templates/election/results.html b/fedora_elections/templates/election/results.html
new file mode 100644
index 0000000..f61d214
--- /dev/null
+++ b/fedora_elections/templates/election/results.html
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+{% block body %}
+ <h1>{{ election.summary }} Results</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{candidate.name}}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ <td>{{ candidate.vote_count }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+{% endblock %}
diff --git a/fedora_elections/templates/election/vote.html b/fedora_elections/templates/election/vote.html
new file mode 100644
index 0000000..e3c4dd4
--- /dev/null
+++ b/fedora_elections/templates/election/vote.html
@@ -0,0 +1,116 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{election.summary}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
+<form action="" method="post">
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ <a href="{{candidate.url}}">[Info]</a>
+ </td>
+ <!-- If user is logged in: -->
+ {% if g.fas_user %}
+ {% if nextaction == 'vote' %}
+ <td>
+ <select name="{{candidate.id}}">
+ {% for opt in range(0, num_candidates+1) %}
+ <option value={{opt}}>{{opt}}</option>
+ {% endfor %}
+ </select>
+ </td>
+ {% endif %}
+ {% if nextaction == 'confirm' %}
+ <td>
+ {% set cid = candidate.id %}
+ <input type="hidden" name="{{candidate.id}}"
+ value="{{voteinfo[cid]}}" />
+ {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
+ {% if weightpercent >= 90 %}
+ <div class="vthot">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 70 %}
+ <div class="vtwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 50 %}
+ <div class="vtmedwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 30 %}
+ <div class="vtmedcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 10 %}
+ <div class="vtcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% else %}
+ <div class="vtcold">
+ {{voteinfo[cid]}}
+ </div>
+ {% endif %}
+ </td>
+ {% endif %}
+ {% endif %}
+ <!-- End if user logged in -->
+ </tr>
+ {% endfor %}
+ </table>
+ {% if g.fas_user %}
+ <p>
+ {% if nextaction == 'confirm' %}
+ <input type="submit" name="{{nextaction}}" value="Submit" />
+ {% else %}
+ <input type="submit" name="{{nextaction}}" value="Preview" />
+ {% endif %}
+ </p>
+ {% endif %}
+</form>
+
+<!-- Make this conditional on whether the user is logged in and can vote in the election -->
+{% if election.legal_voters.count() != 0 %}
+<p>
+ To vote in this election you must be a member of any one of the following groups:
+</p>
+<ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+</ul>
+{% else %}
+<p />
+{% endif %}
+<div>
+ <h2>Information on Range Voting</h2>
+ <p>
+ Fedora Project has implemented
+ <a href="http://rangevoting.org/">Range Voting</a>
+ for this election, in particular the "Range (score-summing, blanks
+ treated as zero score, no quorum rule)" range voting system.
+ </p>
+ <p>
+ To cast your vote in this election simply select a value between 0 and
+ {{num_candidates}} with 0 as 'least or no preference' and
+ {{num_candidates}} as 'highest preference'.
+ </p>
+ <p>
+ At the end of the election, the highest ranking candidate(s) are marked
+ as the winners.
+ </p>
+ <p>
+ For more information about Range Voting, visit the
+ <a href="http://rangevoting.org/">Center for Range Voting</a>.
+ </p>
+</div>
+{% endblock %}
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
new file mode 100644
index 0000000..290c2cf
--- /dev/null
+++ b/fedora_elections/templates/list/index.html
@@ -0,0 +1,32 @@
+{% extends "base.html" %}
+{% block body %}
+ {% if num_elections > 0 %}
+ <h2>{{ title }}</h2>
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{ election.summary }}</td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ <td>
+ {% if election.status == "In progress" %}
+ {% if g.fas_user %}
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">
+ Vote now!
+ </a>
+ {% endif %}
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+{% endblock %}
commit 5045c40fc5aaa0074ea9a40bd54a623424ddbd4a
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Aug 24 03:38:28 2013 +0000
Restructure.
diff --git a/createdb b/createdb
deleted file mode 100755
index 115635b..0000000
--- a/createdb
+++ /dev/null
@@ -1 +0,0 @@
-FEDORA_ELECTIONS_CONFIG=/etc/fedora-elections/fedora-elections.cfg python createdb.py
diff --git a/createdb.in b/createdb.in
deleted file mode 100644
index c7a87da..0000000
--- a/createdb.in
+++ /dev/null
@@ -1 +0,0 @@
-FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora-elections/fedora-elections.cfg python @DATADIR(a)/fedora-elections/createdb.py
diff --git a/createdb.py b/createdb.py
deleted file mode 100644
index a909c5e..0000000
--- a/createdb.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7']
-import pkg_resources
-
-from fedora_elections import db
-db.create_all()
diff --git a/fedora-elections.cfg.in b/fedora-elections.cfg.in
deleted file mode 100644
index ae68f66..0000000
--- a/fedora-elections.cfg.in
+++ /dev/null
@@ -1,18 +0,0 @@
-import os, os.path
-
-DEBUG = True
-
-
-# You will want to change this for your install
-SECRET_KEY = 'change me'
-SQLALCHEMY_DATABASE_URI = 'sqlite:///@SHAREDSTDIR@/fedora-elections/database.sqlite'
-
-FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
-FAS_USERNAME = 'fcelections'
-FAS_PASSWORD = 'el3cTo$hozer'
-FAS_CHECK_CERT = False
-FAS_HTTPS_REQUIRED = False
-
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
-# List of URLs for additional site-wide CSS
-EXTRA_CSS = []
diff --git a/fedora-elections.cfg.sample b/fedora-elections.cfg.sample
deleted file mode 100644
index 1dff33a..0000000
--- a/fedora-elections.cfg.sample
+++ /dev/null
@@ -1,18 +0,0 @@
-import os, os.path
-
-DEBUG = True
-
-
-# You will want to change this for your install
-SECRET_KEY = 'change me'
-SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.getcwd(), 'database.sqlite')
-
-FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
-FAS_USERNAME = 'elections'
-FAS_PASSWORD ='fake_fas_password'
-FAS_CHECK_CERT = False
-FAS_HTTPS_REQUIRED = False
-
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
-# List of URLs for additional site-wide CSS
-EXTRA_CSS = []
diff --git a/fedora-elections.wsgi.in b/fedora-elections.wsgi.in
deleted file mode 100644
index 430cc5c..0000000
--- a/fedora-elections.wsgi.in
+++ /dev/null
@@ -1,21 +0,0 @@
-#-*- coding: UTF-8 -*-
-
-# These two lines are needed to run on EL6
-import __main__
-__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
-import pkg_resources
-
-import os
-import sys
-
-sys.stdout = sys.stderr
-
-## Set the environment variable pointing to the configuration file
-os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
-
-## The following is only needed if you did not install
-## as a python module (for example if you run it from a git clone).
-import sys
-# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
-
-from fedora_elections import app as application
diff --git a/fedora-elections/__init__.py b/fedora-elections/__init__.py
new file mode 100644
index 0000000..86bfdc6
--- /dev/null
+++ b/fedora-elections/__init__.py
@@ -0,0 +1,481 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2012 Red Hat, Inc.
+# Copyright © 2012 Ian Weller <ianweller(a)fedoraproject.org>
+# Copyright © 2012 Toshio Kuratomi <tkuratom(a)redhat.com>
+# Copyright © 2012 Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+#
+# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
+# Toshio Kuratomi <tkuratom(a)redhat.com>
+# Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+
+import flask
+from flask.ext.fas import FAS
+from flask.ext.sqlalchemy import SQLAlchemy
+
+from fedora.client import AuthError, AppError
+from fedora.client.fas2 import AccountSystem
+
+from sqlalchemy.orm.exc import NoResultFound
+
+import fedmsgshim
+
+from datetime import datetime, time
+from functools import wraps
+from urlparse import urlparse, urljoin
+
+app = flask.Flask(__name__)
+app.config.from_object('fedora_elections.default_config')
+app.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
+
+# set up SQLAlchemy
+db = SQLAlchemy(app)
+
+# set up FAS
+fas = FAS(app)
+
+# FAS for usernames.
+# baseURL = "https://admin.fedoraproject.org/accounts"
+# fasuser = "elections"
+# faspw = "elections"
+# fas2_url = 'http://fakefas.fedoraproject.org/accounts/'
+fas2_url = app.config['FAS_BASE_URL']
+fas2_username = app.config['FAS_USERNAME']
+fas2_password = app.config['FAS_PASSWORD']
+fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password,
+ insecure=True)
+
+
+# modular imports
+from fedora_elections import forms, models, redirect
+
+
+def remove_csrf(form_data):
+ return dict([(k, v) for k, v in form_data.items() if k != 'csrf'])
+
+
+def login_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if flask.g.fas_user is None:
+ return flask.redirect(flask.url_for('auth_login',
+ next=flask.request.url))
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+def election_admin_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if flask.g.fas_user is None:
+ return flask.redirect(flask.url_for('auth_login',
+ next=flask.request.url))
+ if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
+ flask.g.fas_user.groups:
+ flask.abort(403)
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+### LIST VIEWS #############################################
+
+(a)app.route('/')
+def index():
+ elections = models.Election.query.filter_by(frontpage=True).all()
+ num_elections = len(elections)
+ return flask.render_template('list/index.html', elections=elections,
+ num_elections=num_elections,
+ title="Elections")
+
+
+(a)app.route('/about/<election_alias>')
+def about_election(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/about.html', election=election,
+ usernamemap=usernamemap)
+
+
+(a)app.route('/archive')
+def archived_elections():
+ now = datetime.utcnow()
+
+ try:
+ elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date<now, models.Election.id>0).all()
+ except NoResultFound:
+ flask.flash('There are no archived elections.')
+ return redirect.safe_redirect_back()
+
+ num_elections = len(elections)
+ if (num_elections == 0):
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('election/archive.html',
+ num_elections=num_elections,
+ elections=elections)
+
+
+(a)app.route('/open')
+def open_elections():
+ now = datetime.utcnow()
+
+ try:
+ elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date>now,
+ models.Election.id>0).all()
+ except NoResultFound:
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ num_elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date>now,
+ models.Election.id>0).count()
+ if (num_elections == 0):
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('list/index.html', elections=elections,
+ num_elections=num_elections,
+ title='Open Elections')
+
+
+### ELECTION VIEWS #############################################
+
+(a)app.route('/vote/<election_alias>', methods=['GET', 'POST'])
+def vote(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'Pending':
+ flask.flash('Voting has not yet started, sorry.')
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Ended':
+ flask.flash('This election is closed. You have been redirected to ' +
+ 'the election results.')
+ return flask.redirect(flask.url_for('results', election.shortname))
+
+ votes = models.Vote.query.filter_by(election_id=election.id,
+ voter=flask.g.fas_user.username).count()
+ if (votes != 0):
+ flask.flash('You have already voted in the election!')
+ return redirect.safe_redirect_back()
+
+ num_candidates = election.candidates.count()
+
+ uvotes = {}
+ for candidate in election.candidates:
+ uvotes[candidate.id] = ""
+ next_action = 'vote'
+
+ if flask.request.method == 'POST':
+ next_action = ''
+ form_values = flask.request.form.values()
+ if 'Submit' in form_values:
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote >= 0 and vote <= num_candidates:
+ uvotes[candidate.id] = int(vote)
+ else:
+ flask.flash("Invalid Ballot!")
+ return redirect.safe_redirect_back()
+
+ except ValueError:
+ flask.flash("Invalid Ballot!")
+ return redirect.safe_redirect_back()
+
+ for uvote in uvotes:
+ new_vote = models.Vote(election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=uvote,
+ value=uvotes[uvote] )
+ db.session.add(new_vote)
+ db.session.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return redirect.safe_redirect_back()
+
+ elif 'Preview' in form_values:
+ flask.flash("Please confirm your vote!")
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote > num_candidates:
+ flask.flash("Invalid data.")
+ uvotes[candidate.id] = 0
+ next_action = 'vote'
+ elif vote >= 0:
+ uvotes[candidate.id] = vote
+ else:
+ flask.flash("Invalid data2.")
+ uvotes[c.id] = 0
+ next_action = 'vote'
+ except ValueError:
+ flask.flash("Invalid data")
+
+ if (next_action != 'vote'):
+ next_action = 'confirm'
+
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/vote.html', election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ voteinfo=uvotes, nextaction=next_action)
+
+
+### AUTH VIEWS #############################################
+
+(a)app.route('/login', methods=('GET', 'POST'))
+def auth_login():
+ if flask.g.fas_user:
+ return redirect.safe_redirect_back()
+ form = forms.LoginForm()
+ if form.validate_on_submit():
+ if fas.login(form.username.data, form.password.data):
+ flask.flash('Welcome, %s' % flask.g.fas_user.username)
+ return redirect.safe_redirect_back()
+ else:
+ flask.flash('Incorrect username or password')
+ return flask.render_template('auth/login.html', form=form)
+
+
+(a)app.route('/logout')
+def auth_logout():
+ if not flask.g.fas_user:
+ return redirect.safe_redirect_back()
+ fas.logout()
+ flask.flash('You have been logged out')
+ return redirect.safe_redirect_back()
+
+
+### ELECTION ADMIN VIEWS ###################################
+
+(a)app.route('/admin/')
+@election_admin_required
+def admin_view_elections():
+ elections = models.Election.query.\
+ filter_by(fas_user=flask.g.fas_user.username).all()
+ num_elections = len(elections)
+ if (num_elections == 0):
+ flask.flash('You do not have any elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('admin/all_elections.html',
+ num_elections=num_elections,
+ elections=elections)
+
+
+(a)app.route('/admin/new', methods=("GET", "POST"))
+@election_admin_required
+def admin_new_election():
+ form = forms.ElectionForm()
+ if form.validate_on_submit():
+ election = models.Election(**remove_csrf(form.data))
+ # Fix start_date and end_date to use datetime
+ election.start_date = datetime.combine(election.start_date, time())
+ election.end_date = datetime.combine(election.end_date,
+ time(23, 59, 59))
+ election.fas_user = flask.g.fas_user.username
+ db.session.add(election)
+ admin = models.ElectionAdminGroup(election=election,
+ group_name=app.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ role_required=u'user')
+ db.session.add(admin)
+ db.session.commit()
+ flask.flash('Election "%s" added' % election.summary)
+ fedmsgshim.publish(topic="election.new", msg=election)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/election_form.html', form=form,
+ submit_text='Create election')
+
+
+(a)app.route('/admin/<election_alias>/')
+@election_admin_required
+def admin_view_election(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ return flask.render_template('admin/view_election.html', election=election)
+
+
+(a)app.route('/admin/<election_alias>/edit', methods=("GET", "POST"))
+@election_admin_required
+def admin_edit_election(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ form = forms.ElectionForm(election.id, obj=election)
+ if form.validate_on_submit():
+ form.populate_obj(election)
+ db.session.commit()
+ flask.flash('Election "%s" saved' % election.summary)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/election_form.html', form=form,
+ submit_text='Edit election')
+
+
+(a)app.route('/admin/<election_alias>/candidates/new', methods=("GET", "POST"))
+@election_admin_required
+def admin_add_candidate(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ form = forms.CandidateForm()
+ if form.validate_on_submit():
+ candidate = models.Candidate(election=election,
+ **remove_csrf(form.data))
+ db.session.add(candidate)
+ db.session.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/candidate.html', form=form,
+ submit_text='Add candidate')
+
+
+(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
+ methods=("GET", "POST"))
+@election_admin_required
+def admin_edit_candidate(election_alias, candidate_id):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ if not election or not candidate:
+ flask.abort(404)
+ if candidate.election != election:
+ flask.abort(404)
+ form = forms.CandidateForm(obj=candidate)
+ if form.validate_on_submit():
+ form.populate_obj(candidate)
+ db.session.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/candidate.html', form=form,
+ submit_text='Edit candidate')
+
+
+(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
+ methods=("GET", "POST"))
+@election_admin_required
+def admin_delete_candidate(election_alias, candidate_id):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ if not election or not candidate:
+ flask.abort(404)
+ if candidate.election != election:
+ flask.abort(404)
+ form = forms.ConfirmationForm()
+ if form.validate_on_submit():
+ db.session.delete(candidate)
+ candidate_name = candidate.name
+ db.session.commit()
+ flask.flash('Candidate "%s" deleted' % candidate_name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/delete_candidate.html', form=form,
+ candidate=candidate)
+
+
+(a)app.route('/results/<election_alias>')
+def election_results(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'In progress':
+ flask.flash("We are sorry. The results for this election cannot be" \
+ " viewed at this time because the election is still in" \
+ " progress.")
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Pending':
+ flask.flash("We are sorry. The results for this election cannot be" \
+ " viewed at this time because the election has not" \
+ " started.")
+ return redirect.safe_redirect_back()
+
+ elif election.embargoed == 1:
+ if flask.g.fas_user is None:
+ flask.flash("We are sorry. The results for this election" \
+ "cannot be viewed because they are currently" \
+ " embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
+ else:
+ if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
+ flask.g.fas_user.groups:
+ pass
+ else:
+ match = 0
+ admingroups = models.ElectionAdminGroup.query.filter_by(
+ election_id=election.id).all()
+ for admingroup in admingroups:
+ if admingroup.group_name in flask.g.fas_user.groups:
+ match = 1
+
+ if match == 0:
+ flask.flash("We are sorry. The results for this election" \
+ "cannot be viewed because they are currently" \
+ " embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/results.html', election=election,
+ usernamemap=usernamemap)
diff --git a/fedora-elections/createdb.in b/fedora-elections/createdb.in
new file mode 100644
index 0000000..c7a87da
--- /dev/null
+++ b/fedora-elections/createdb.in
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora-elections/fedora-elections.cfg python @DATADIR(a)/fedora-elections/createdb.py
diff --git a/fedora-elections/createdb.py b/fedora-elections/createdb.py
new file mode 100644
index 0000000..a909c5e
--- /dev/null
+++ b/fedora-elections/createdb.py
@@ -0,0 +1,6 @@
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7']
+import pkg_resources
+
+from fedora_elections import db
+db.create_all()
diff --git a/fedora-elections/default_config.py b/fedora-elections/default_config.py
new file mode 100644
index 0000000..7b5b794
--- /dev/null
+++ b/fedora-elections/default_config.py
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
diff --git a/fedora-elections/fedmsgshim.py b/fedora-elections/fedmsgshim.py
new file mode 100644
index 0000000..775f785
--- /dev/null
+++ b/fedora-elections/fedmsgshim.py
@@ -0,0 +1,17 @@
+""" This is a temporary shim that allows fedmsg to be an optional dependency of
+elections. If fedmsg is installed, these function calls will try to actually send
+messages. If it is not installed, it will return silently.
+
+ :Author: Ralph Bean <rbean(a)redhat.com>
+
+"""
+
+import warnings
+
+
+def publish(*args, **kwargs):
+ try:
+ import fedmsg
+ fedmsg.publish(*args, **kwargs)
+ except Exception, e:
+ warnings.warn(str(e))
diff --git a/fedora-elections/fedora-elections.cfg.in b/fedora-elections/fedora-elections.cfg.in
new file mode 100644
index 0000000..ae68f66
--- /dev/null
+++ b/fedora-elections/fedora-elections.cfg.in
@@ -0,0 +1,18 @@
+import os, os.path
+
+DEBUG = True
+
+
+# You will want to change this for your install
+SECRET_KEY = 'change me'
+SQLALCHEMY_DATABASE_URI = 'sqlite:///@SHAREDSTDIR@/fedora-elections/database.sqlite'
+
+FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
+FAS_USERNAME = 'fcelections'
+FAS_PASSWORD = 'el3cTo$hozer'
+FAS_CHECK_CERT = False
+FAS_HTTPS_REQUIRED = False
+
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+# List of URLs for additional site-wide CSS
+EXTRA_CSS = []
diff --git a/fedora-elections/fedora-elections.wsgi.in b/fedora-elections/fedora-elections.wsgi.in
new file mode 100644
index 0000000..430cc5c
--- /dev/null
+++ b/fedora-elections/fedora-elections.wsgi.in
@@ -0,0 +1,21 @@
+#-*- coding: UTF-8 -*-
+
+# These two lines are needed to run on EL6
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import os
+import sys
+
+sys.stdout = sys.stderr
+
+## Set the environment variable pointing to the configuration file
+os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
+
+## The following is only needed if you did not install
+## as a python module (for example if you run it from a git clone).
+import sys
+# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
+
+from fedora_elections import app as application
diff --git a/fedora-elections/forms.py b/fedora-elections/forms.py
new file mode 100644
index 0000000..f050dbd
--- /dev/null
+++ b/fedora-elections/forms.py
@@ -0,0 +1,69 @@
+from flask.ext import wtf
+
+from fedora_elections.models import Election
+
+
+class ElectionForm(wtf.Form):
+ summary = wtf.TextField('Summary', [wtf.validators.Required(),
+ wtf.validators.Length(max=150)])
+ alias = wtf.TextField('Alias', [
+ wtf.validators.Required(),
+ wtf.validators.Length(max=100),
+ wtf.validators.Regexp('[a-z0-9_-]+',
+ message=('Alias may only contain lower case '
+ 'letters, numbers, hyphens and '
+ 'underscores.')),
+ ])
+ description = wtf.TextAreaField('Description', [wtf.validators.Required()])
+ url = wtf.TextField('URL', [wtf.validators.Required(),
+ wtf.validators.URL(),
+ wtf.validators.Length(max=250)])
+ start_date = wtf.DateField('Start date', [wtf.validators.Required()])
+ end_date = wtf.DateField('End date', [wtf.validators.Required()])
+ number_elected = wtf.IntegerField('Number elected',
+ [wtf.validators.Required(),
+ wtf.validators.NumberRange(min=1)],
+ default=1)
+ candidates_are_fasusers = wtf.BooleanField('Candidates are FAS users?')
+ frontpage = wtf.BooleanField('Show on front page?')
+ embargoed = wtf.BooleanField('Embargo results?', default=True)
+
+ def __init__(form, election_id=None, *args, **kwargs):
+ super(ElectionForm, form).__init__(*args, **kwargs)
+ form._election_id = election_id
+
+ def validate_summary(form, field):
+ check = Election.query.filter_by(summary=form.summary.data).all()
+ if check:
+ if not (form._election_id and form._election_id == check[0].id):
+ raise wtf.ValidationError('There is already another election '
+ 'with this summary.')
+
+ def validate_alias(form, field):
+ if form.alias.data == 'new':
+ raise wtf.ValidationError(flask.Markup('The alias cannot be '
+ '<code>new</code>.'))
+ check = Election.query.filter_by(alias=form.alias.data).all()
+ if check:
+ if not (form._election_id and form._election_id == check[0].id):
+ raise wtf.ValidationError('There is already another election '
+ 'with this alias.')
+
+ def validate_end_date(form, field):
+ if form.end_date.data <= form.start_date.data:
+ raise wtf.ValidationError('End date must be later than '
+ 'start date.')
+
+
+class CandidateForm(wtf.Form):
+ name = wtf.TextField('Name', [wtf.validators.Required()])
+ url = wtf.TextField('URL', [wtf.validators.Length(max=250)])
+
+
+class LoginForm(wtf.Form):
+ username = wtf.TextField('Username', [wtf.validators.Required()])
+ password = wtf.PasswordField('Password', [wtf.validators.Required()])
+
+
+class ConfirmationForm(wtf.Form):
+ pass
diff --git a/fedora-elections/httpd-fedora-elections.conf.in b/fedora-elections/httpd-fedora-elections.conf.in
new file mode 100644
index 0000000..c974b88
--- /dev/null
+++ b/fedora-elections/httpd-fedora-elections.conf.in
@@ -0,0 +1,23 @@
+Alias /fedora-elections/images @DATADIR@/fedora-elections/images
+Alias /fedora-elections/static @DATADIR@/fedora-elections/static
+
+WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
+WSGISocketPrefix run/wsgi
+WSGIRestrictStdout On
+WSGIRestrictSignal Off
+WSGIPythonOptimize 1
+
+WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora-elections/fedora-elections.wsgi
+
+<Location /fedora-elections>
+ WSGIProcessGroup fedora-elections
+ <IfModule mod_authz_core.c>
+ # Apache 2.4
+ Require all granted
+ </IfModule>
+ <IfModule !mod_authz_core.c>
+ # Apache 2.2
+ Order deny,allow
+ Allow from all
+ </IfModule>
+</Location>
diff --git a/fedora-elections/images/border-left.png b/fedora-elections/images/border-left.png
new file mode 100644
index 0000000..6f256df
Binary files /dev/null and b/fedora-elections/images/border-left.png differ
diff --git a/fedora-elections/images/border-right.png b/fedora-elections/images/border-right.png
new file mode 100644
index 0000000..0e7365b
Binary files /dev/null and b/fedora-elections/images/border-right.png differ
diff --git a/fedora-elections/images/fedora-logo.png b/fedora-elections/images/fedora-logo.png
new file mode 100644
index 0000000..ce95893
Binary files /dev/null and b/fedora-elections/images/fedora-logo.png differ
diff --git a/fedora-elections/images/line.png b/fedora-elections/images/line.png
new file mode 100644
index 0000000..3498cb9
Binary files /dev/null and b/fedora-elections/images/line.png differ
diff --git a/fedora-elections/models.py b/fedora-elections/models.py
new file mode 100644
index 0000000..b19bd33
--- /dev/null
+++ b/fedora-elections/models.py
@@ -0,0 +1,103 @@
+from fedora_elections import app, db
+
+from datetime import datetime
+
+class Election(db.Model):
+ __tablename__ = 'elections'
+
+ id = db.Column(db.Integer, primary_key=True)
+ summary = db.Column(db.Unicode(150), unique=True, nullable=False)
+ alias = db.Column(db.Unicode(100), unique=True, nullable=False)
+ description = db.Column(db.UnicodeText, nullable=False)
+ url = db.Column(db.Unicode(250), nullable=False)
+ start_date = db.Column(db.DateTime, nullable=False)
+ end_date = db.Column(db.DateTime, nullable=False)
+ number_elected = db.Column(db.Integer, nullable=False, default=1)
+ embargoed = db.Column(db.Boolean, nullable=False, default=True)
+ frontpage = db.Column(db.Boolean, nullable=False, default=False)
+ voting_type = db.Column(db.Unicode(100), nullable=False, default=u'range')
+ candidates_are_fasusers = db.Column(db.Boolean, nullable=False,
+ default=False)
+ fas_user = db.Column(db.Unicode(50), nullable=False)
+
+ @property
+ def status(self):
+ if datetime.now() < self.start_date:
+ return 'Pending'
+ else:
+ if datetime.now() < self.end_date:
+ return 'In progress'
+ else:
+ if self.embargoed:
+ return 'Embargoed'
+ else:
+ return 'Ended'
+
+ @property
+ def locked(self):
+ return datetime.now() >= self.start_date
+
+
+class ElectionAdminGroup(db.Model):
+ __tablename__ = 'electionadmingroups'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('admin_groups',
+ lazy='dynamic'))
+ group_name = db.Column(db.Unicode(150), nullable=False)
+ role_required = db.Column(db.Enum(u'user', u'sponsor', u'administrator'),
+ nullable=False)
+
+
+class Candidate(db.Model):
+ __tablename__ = 'candidates'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('candidates',
+ lazy='dynamic'))
+ name = db.Column(db.Unicode(150), nullable=False) # FAS username if
+ # candidates_are_fasusers
+ url = db.Column(db.Unicode(250))
+
+ @property
+ def vote_count(self):
+ values = [v.value for v in self.votes]
+ return sum(values)
+
+
+class LegalVoter(db.Model):
+ __tablename__ = 'legalvoters'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('legal_voters',
+ lazy='dynamic'))
+ # special names:
+ # "cla + one" = cla_done + 1 non-cla group
+ group_name = db.Column(db.Unicode(150), nullable=False)
+
+
+class Vote(db.Model):
+ __tablename__ = 'votes'
+
+ __table_args__ = (db.UniqueConstraint('election_id', 'voter',
+ 'candidate_id',
+ name='eid_voter_cid'), {} )
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('votes',
+ lazy='dynamic'))
+ voter = db.Column(db.Unicode(150), nullable=False)
+ timestamp = db.Column(db.DateTime, nullable=False)
+ candidate_id = db.Column(db.Integer, db.ForeignKey('candidates.id'),
+ nullable=False)
+ candidate = db.relationship('Candidate', backref=db.backref('votes',
+ lazy='dynamic'))
+ value = db.Column(db.Integer, nullable=False)
diff --git a/fedora-elections/redirect.py b/fedora-elections/redirect.py
new file mode 100644
index 0000000..5571abe
--- /dev/null
+++ b/fedora-elections/redirect.py
@@ -0,0 +1,23 @@
+import flask
+
+from urlparse import urljoin, urlparse
+
+
+def is_safe_url(target):
+ ref_url = urlparse(flask.request.host_url)
+ test_url = urlparse(urljoin(flask.request.host_url, target))
+ return test_url.scheme in ('http', 'https') and \
+ ref_url.netloc == test_url.netloc
+
+
+def safe_redirect_back(next=None, fallback=('index', {})):
+ targets = []
+ if next:
+ targets.append(next)
+ if 'next' in flask.request.args and \
+ flask.request.args['next']:
+ targets.append(flask.request.args['next'])
+ targets.append(flask.url_for(fallback[0], **fallback[1]))
+ for target in targets:
+ if is_safe_url(target):
+ return flask.redirect(target)
diff --git a/fedora-elections/static/style.css b/fedora-elections/static/style.css
new file mode 100644
index 0000000..ee09d79
--- /dev/null
+++ b/fedora-elections/static/style.css
@@ -0,0 +1,144 @@
+body {
+ font-family: sans-serif;
+ font-size: 12pt;
+ line-height: 1.35;
+}
+
+#flashes {
+ background-color: #ffa;
+}
+
+nav {
+ background-color: #eee;
+ padding: 0.5em 1em;
+}
+
+nav h2 {
+ display: inline;
+ font-size: inherit;
+ margin-right: 1em;
+}
+
+nav ul {
+ display: inline;
+ list-style: none;
+ padding: 0;
+}
+
+nav li {
+ display: inline;
+ margin-right: .5em;
+}
+
+.clear {
+ clear: both;
+}
+
+/* input elements */
+
+fieldset {
+ border: 0;
+ padding: 0;
+ margin: .5em 0;
+ float: left;
+ clear: both;
+}
+
+.row {
+ margin: .25em 0;
+}
+
+.row label:first-child {
+ width: 8em;
+ margin-right: 0.75em;
+ text-align: right;
+ clear: both;
+ font-weight: bold;
+ display: inline-block;
+ vertical-align: top;
+}
+
+.row textarea {
+ width: 30em;
+ height: 8em;
+}
+
+form .row .errors {
+ list-style: none;
+ margin: .5em 0 1em 10.9375em;
+ padding: 0;
+ font-size: .8em;
+ color: #c00;
+ width: 30em;
+}
+
+div.help {
+ width: 40%;
+ float: right;
+ background-color: #ffa;
+ border-top: 1px solid #ee9;
+ padding: 1em;
+/* font-size: .8em; */
+ margin: .625em 0;
+}
+
+div > *:first-child { margin-top: 0; }
+div > *:last-child { margin-bottom: 0; }
+
+form .buttons.indent {
+ margin-left: 8.75em;
+}
+
+.vthot
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #9C0000;
+}
+
+.vtwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #FFA300;
+}
+
+.vtmedwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #93FF63;
+}
+
+.vtmedcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #00DAFE;
+}
+
+.vtcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #0033FF;
+}
+
+.vtcold
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #000083;
+}
diff --git a/fedora-elections/templates/_formhelpers.html b/fedora-elections/templates/_formhelpers.html
new file mode 100644
index 0000000..ff781b7
--- /dev/null
+++ b/fedora-elections/templates/_formhelpers.html
@@ -0,0 +1,13 @@
+{% macro render_field(field, after="") %}
+<div class="row">
+ {{ field.label }}{{ field(**kwargs)|safe }}{% if after %} {{ after }}{% endif %}
+ {% if field.errors %}
+ <ul class="errors">
+ {% for error in field.errors %}
+ <li>{{ error }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+</div>
+{% endmacro %}
+{# vim: set ts=2 sw=2 sta et: #}
diff --git a/fedora-elections/templates/admin/all_elections.html b/fedora-elections/templates/admin/all_elections.html
new file mode 100644
index 0000000..e168ac0
--- /dev/null
+++ b/fedora-elections/templates/admin/all_elections.html
@@ -0,0 +1,28 @@
+{% extends "base.html" %}
+{% block title %}Election administration{% endblock %}
+{% block body %}
+ {% if num_elections > 0 %}
+ <h2>Election administration</h2>
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>
+ <a href="{{ url_for('admin_view_election',
+ election_alias=election.alias) }}">
+ {{ election.summary }}
+ </a>
+ </td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+{%endblock %}
diff --git a/fedora-elections/templates/admin/candidate.html b/fedora-elections/templates/admin/candidate.html
new file mode 100644
index 0000000..a7b2fda
--- /dev/null
+++ b/fedora-elections/templates/admin/candidate.html
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}{{ submit_text }}{% endblock %}
+{% block body %}
+ <h2>{{ submit_text }}</h2>
+
+ <form action="" method="post">
+ <fieldset>
+ {{ render_field(form.name) }}
+ {{ render_field(form.url) }}
+ </fieldset>
+
+ <br class="clear">
+ <p class="buttons">
+ <input type="submit" class="submit positive" value="{{ submit_text }}">
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora-elections/templates/admin/delete_candidate.html b/fedora-elections/templates/admin/delete_candidate.html
new file mode 100644
index 0000000..d764f19
--- /dev/null
+++ b/fedora-elections/templates/admin/delete_candidate.html
@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}Delete candidate{% endblock %}
+{% block body %}
+ <h2>{{ self.title() }}</h2>
+
+ <form action="" method="post">
+ <p>Are you sure you want to delete candidate "{{ candidate.name }}"?</p>
+ <p class="buttons">
+ <input type="submit" value="Delete candidate">
+ <a href="{{ url_for('admin_view_election',
+ election_alias=candidate.election.alias) }}">
+ Cancel
+ </a>
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora-elections/templates/admin/election_form.html b/fedora-elections/templates/admin/election_form.html
new file mode 100644
index 0000000..6e2a3d6
--- /dev/null
+++ b/fedora-elections/templates/admin/election_form.html
@@ -0,0 +1,77 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}{{ submit_text }}{% endblock %}
+{% block body %}
+ <h2>{{ self.title() }}</h2>
+
+ <form action="" method="post">
+ <fieldset>
+ {{ render_field(form.summary) }}
+ </fieldset>
+ <div class="help">
+ <p>This is the name of the election that users will first see.</p>
+ <p>Example: <kbd>Fedora Board, November 2010</kbd></p>
+ </div>
+ <fieldset>
+ {{ render_field(form.alias) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ A short, unique name that will be used for the election's URL.
+ Lowercase letters, numbers, hyphens and underscores only.
+ </p>
+ <p>Example: <kbd>boardf15</kbd></p>
+ </div>
+ <fieldset>
+ {{ render_field(form.description) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.url) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.start_date, " at 00:00:00 UTC") }}
+ {{ render_field(form.end_date, " at 23:59:59 UTC") }}
+ </fieldset>
+ <div class="help">
+ <p>Enter dates in <kbd>YYYY-MM-DD</kbd> format.</p>
+ </div>
+ <fieldset>
+ {{ render_field(form.number_elected) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ This determines the cutoff for how many candidates win the election.
+ For example, if you are electing four empty seats on a committee,
+ enter <kbd>4</kbd> here.
+ </p>
+ </div>
+ <fieldset>
+ {{ render_field(form.candidates_are_fasusers) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ Check this if every candidate in this election has a FAS user account.
+ This will automatically pull full names and wiki page URLs from FAS.
+ </p>
+ </div>
+ <fieldset>
+ {{ render_field(form.frontpage) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.embargoed) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ Embargo results when the election is finished. To unembargo them, edit
+ the election and uncheck this box.
+ </p>
+ </div>
+
+ <br class="clear">
+ {% block election_form_extra %}{% endblock %}
+ <p class="buttons">
+ <input type="submit" class="submit positive" value="Save Changes">
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora-elections/templates/admin/view_election.html b/fedora-elections/templates/admin/view_election.html
new file mode 100644
index 0000000..ed070f3
--- /dev/null
+++ b/fedora-elections/templates/admin/view_election.html
@@ -0,0 +1,42 @@
+{% extends "base.html" %}
+{% block body %}
+ <h2>{{ election.summary }}</h2>
+ <p>{{ election.description }}</p>
+ <p>
+ <a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">
+ Edit election
+ </a>
+ </p>
+ <ul>
+ <li>On front page: {{ election.frontpage }}</li>
+ <li>Start date: {{ election.start_date }}</li>
+ <li>End date: {{ election.end_date }}</li>
+ <li>Number elected: {{ election.number_elected }}</li>
+ <li>Election type: {{ election.voting_type }}</li>
+ </ul>
+ <h3>Candidates</h3>
+ {% if election.candidates.all() %}
+ <ul>
+ {% for candidate in election.candidates.all() %}
+ <li>{{ candidate.name }}
+ <a href="{{ url_for('admin_edit_candidate',
+ election_alias=election.alias,
+ candidate_id=candidate.id) }}">
+ [edit]
+ </a>
+ <a href="{{ url_for('admin_delete_candidate',
+ election_alias=election.alias,
+ candidate_id=candidate.id) }}">
+ [x]
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>There are no candidates.</p>
+ {% endif %}
+ <a href="{{ url_for('admin_add_candidate',
+ election_alias=election.alias) }}">
+ Add a candidate
+ </a>
+{% endblock %}
diff --git a/fedora-elections/templates/auth/login.html b/fedora-elections/templates/auth/login.html
new file mode 100644
index 0000000..9e7b73c
--- /dev/null
+++ b/fedora-elections/templates/auth/login.html
@@ -0,0 +1,20 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}Log in{% endblock %}
+{% block body %}
+ <h2>Log in</h2>
+
+ <form action="" method="post">
+ <p>
+ Log in using your
+ <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a>
+ account.
+ </p>
+ {{ render_field(form.username) }}
+ {{ render_field(form.password) }}
+ <p class="buttons indent"><input type="submit" class="submit positive"
+ value="Log in">
+ </p>
+ {{ form.csrf_token }}
+ </form>
+{% endblock %}
diff --git a/fedora-elections/templates/base.html b/fedora-elections/templates/base.html
new file mode 100644
index 0000000..3e6f806
--- /dev/null
+++ b/fedora-elections/templates/base.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ {% block head %}
+ <title>
+ Fedora Elections
+ {% if self.title() %}
+ —
+ {% block title %}{% endblock %}
+ {% endif %}
+ </title>
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="{{ url_for("static", filename="style.css") }}" />
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="https://fedoraproject.org/static/css/fedora.css" />
+ {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
+ {% endfor %}
+ {% endblock %}
+ </head>
+ <body>
+ <div id="wrapper">
+ <div id="head">
+ <h1><a href="https://admin.fedoraproject.org/en/index">Fedora</a></h1>
+ </div>
+
+ <div id="sidebar">
+ <div id="nav">
+ <h2>Navigation</h2>
+ <ul>
+ <li id="nav-home">
+ <a href="http://fedoraproject.org/en/index">Home</a>
+ </li>
+ <li id="nav-get">
+ <a href="http://fedoraproject.org/en/get-fedora">Get Fedora</a>
+ </li>
+ <li id="nav-join">
+ <a href="http://fedoraproject.org/en/join-fedora">Join Fedora</a>
+ </li>
+ <li id="nav-help">
+ <a href="http://fedoraproject.org/en/get-help">Get Help</a>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <div id="content">
+ <ul class="toolbar" id="#main-toolbar">
+ {% if g.fas_user %}
+ <li>
+ Hello, {{ g.fas_user.username }} —
+ <a href="{{ url_for("auth_logout") }}">log out</a>
+ </li>
+ {% else %}
+ <li>
+ You are not logged in —
+ <a href="{{ url_for("auth_login") }}">log in</a>
+ </li>
+ {% endif %}
+ </ul>
+ <h2>Elections</h2>
+ <ul>
+ <li>
+ <a href="{{ url_for("open_elections") }}">
+ Open elections
+ </a>
+ </li>
+ <li>
+ <a href="{{ url_for("archived_elections") }}">
+ Archived elections
+ </a>
+ </li>
+ </ul>
+ <br>
+ <h2>Administration</h2>
+ <ul>
+ <li>
+ <a href="{{ url_for("admin_view_elections") }}">
+ My elections
+ </a>
+ </li>
+ <li>
+ <a href="{{ url_for("admin_new_election") }}">
+ Create new election
+ </a>
+ </li>
+ </ul>
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+ <ul id="flashes">
+ {% for message in messages %}
+ <li>{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {% endwith %}
+ {% block body %}
+ {% endblock %}
+ </div>
+ </div>
+ <div id="bottom">
+ <div id="footer">
+ {% block footer %}
+ <p class="copy">
+ Copyright © 2008 Red Hat, Inc. and others. Please send any
+ comments or corrections to the
+ <a href="mailto:webmaster@fedoraproject.org">
+ websites team
+ </a>.
+ </p>
+ <p class="disclaimer">
+ The Fedora Project is maintained and driven by the community and
+ sponsored by Red Hat. This is a community maintained site. Red
+ Hat is not responsible for content.
+ </p>
+ <ul>
+ <li class="first">
+ <a href="/en/sponsors">Sponsors</a>
+ </li>
+ <li>
+ <a href="http://fedoraproject.org/wiki/Legal">
+ Legal
+ </a>
+ </li>
+ <li>
+ <a href="http://fedoraproject.org/wiki/Legal/TrademarkGuidelines">
+ Trademark Guidelines
+ </a>
+ </li>
+ </ul>
+ </div>
+ {% endblock %}
+ </div>
+ </body>
+</html>
diff --git a/fedora-elections/templates/election/about.html b/fedora-elections/templates/election/about.html
new file mode 100644
index 0000000..c7bf0ec
--- /dev/null
+++ b/fedora-elections/templates/election/about.html
@@ -0,0 +1,40 @@
+{% extends "base.html" %}
+{% block title %}Election Information{% endblock %}
+{% block body %}
+ <h1>{{ election.summary }}</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ {% if election.status == 'Ended' %}
+ <a href="/results/{{ election.alias }}">Show Results!</a>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p />
+ {% endif %}
+{% endblock %}
diff --git a/fedora-elections/templates/election/archive.html b/fedora-elections/templates/election/archive.html
new file mode 100644
index 0000000..8740b3c
--- /dev/null
+++ b/fedora-elections/templates/election/archive.html
@@ -0,0 +1,28 @@
+{% extends "base.html" %}
+{% block body %}
+ {% if num_elections > 0 %}
+ <h2>Past Elections</h2>
+ <table border="1" cellpadding="1">
+ <tr>
+ <th></th>
+ <th>Start Date (UTC)</th>
+ <th>End Date (UTC)</th>
+ <th></th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{election.description}}</td>
+ <td>{{election.start_date}}</td>
+ <td>{{election.end_date}}</td>
+ <td>
+ <a href="about/{{ election.alias }}"">More information</a>
+ </td>
+ <td>
+ <a href="results/{{ election.alias }}">Results</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+{% endblock %}
diff --git a/fedora-elections/templates/election/results.html b/fedora-elections/templates/election/results.html
new file mode 100644
index 0000000..f61d214
--- /dev/null
+++ b/fedora-elections/templates/election/results.html
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+{% block body %}
+ <h1>{{ election.summary }} Results</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{candidate.name}}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ <td>{{ candidate.vote_count }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+{% endblock %}
diff --git a/fedora-elections/templates/election/vote.html b/fedora-elections/templates/election/vote.html
new file mode 100644
index 0000000..e3c4dd4
--- /dev/null
+++ b/fedora-elections/templates/election/vote.html
@@ -0,0 +1,116 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{election.summary}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
+<form action="" method="post">
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ <a href="{{candidate.url}}">[Info]</a>
+ </td>
+ <!-- If user is logged in: -->
+ {% if g.fas_user %}
+ {% if nextaction == 'vote' %}
+ <td>
+ <select name="{{candidate.id}}">
+ {% for opt in range(0, num_candidates+1) %}
+ <option value={{opt}}>{{opt}}</option>
+ {% endfor %}
+ </select>
+ </td>
+ {% endif %}
+ {% if nextaction == 'confirm' %}
+ <td>
+ {% set cid = candidate.id %}
+ <input type="hidden" name="{{candidate.id}}"
+ value="{{voteinfo[cid]}}" />
+ {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
+ {% if weightpercent >= 90 %}
+ <div class="vthot">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 70 %}
+ <div class="vtwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 50 %}
+ <div class="vtmedwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 30 %}
+ <div class="vtmedcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 10 %}
+ <div class="vtcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% else %}
+ <div class="vtcold">
+ {{voteinfo[cid]}}
+ </div>
+ {% endif %}
+ </td>
+ {% endif %}
+ {% endif %}
+ <!-- End if user logged in -->
+ </tr>
+ {% endfor %}
+ </table>
+ {% if g.fas_user %}
+ <p>
+ {% if nextaction == 'confirm' %}
+ <input type="submit" name="{{nextaction}}" value="Submit" />
+ {% else %}
+ <input type="submit" name="{{nextaction}}" value="Preview" />
+ {% endif %}
+ </p>
+ {% endif %}
+</form>
+
+<!-- Make this conditional on whether the user is logged in and can vote in the election -->
+{% if election.legal_voters.count() != 0 %}
+<p>
+ To vote in this election you must be a member of any one of the following groups:
+</p>
+<ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+</ul>
+{% else %}
+<p />
+{% endif %}
+<div>
+ <h2>Information on Range Voting</h2>
+ <p>
+ Fedora Project has implemented
+ <a href="http://rangevoting.org/">Range Voting</a>
+ for this election, in particular the "Range (score-summing, blanks
+ treated as zero score, no quorum rule)" range voting system.
+ </p>
+ <p>
+ To cast your vote in this election simply select a value between 0 and
+ {{num_candidates}} with 0 as 'least or no preference' and
+ {{num_candidates}} as 'highest preference'.
+ </p>
+ <p>
+ At the end of the election, the highest ranking candidate(s) are marked
+ as the winners.
+ </p>
+ <p>
+ For more information about Range Voting, visit the
+ <a href="http://rangevoting.org/">Center for Range Voting</a>.
+ </p>
+</div>
+{% endblock %}
diff --git a/fedora-elections/templates/list/index.html b/fedora-elections/templates/list/index.html
new file mode 100644
index 0000000..290c2cf
--- /dev/null
+++ b/fedora-elections/templates/list/index.html
@@ -0,0 +1,32 @@
+{% extends "base.html" %}
+{% block body %}
+ {% if num_elections > 0 %}
+ <h2>{{ title }}</h2>
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{ election.summary }}</td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ <td>
+ {% if election.status == "In progress" %}
+ {% if g.fas_user %}
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">
+ Vote now!
+ </a>
+ {% endif %}
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+{% endblock %}
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
deleted file mode 100644
index 86bfdc6..0000000
--- a/fedora_elections/__init__.py
+++ /dev/null
@@ -1,481 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2012 Red Hat, Inc.
-# Copyright © 2012 Ian Weller <ianweller(a)fedoraproject.org>
-# Copyright © 2012 Toshio Kuratomi <tkuratom(a)redhat.com>
-# Copyright © 2012 Frank Chiulli <fchiulli(a)fedoraproject.org>
-#
-# This copyrighted material is made available to anyone wishing to use, modify,
-# copy, or redistribute it subject to the terms and conditions of the GNU
-# General Public License v.2, or (at your option) any later version. This
-# program is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY expressed or implied, including the implied warranties of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
-# Public License for more details. You should have received a copy of the GNU
-# General Public License along with this program; if not, write to the Free
-# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
-# code or documentation are not subject to the GNU General Public License and
-# may only be used or replicated with the express permission of Red Hat, Inc.
-#
-# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
-# Toshio Kuratomi <tkuratom(a)redhat.com>
-# Frank Chiulli <fchiulli(a)fedoraproject.org>
-#
-
-import flask
-from flask.ext.fas import FAS
-from flask.ext.sqlalchemy import SQLAlchemy
-
-from fedora.client import AuthError, AppError
-from fedora.client.fas2 import AccountSystem
-
-from sqlalchemy.orm.exc import NoResultFound
-
-import fedmsgshim
-
-from datetime import datetime, time
-from functools import wraps
-from urlparse import urlparse, urljoin
-
-app = flask.Flask(__name__)
-app.config.from_object('fedora_elections.default_config')
-app.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
-
-# set up SQLAlchemy
-db = SQLAlchemy(app)
-
-# set up FAS
-fas = FAS(app)
-
-# FAS for usernames.
-# baseURL = "https://admin.fedoraproject.org/accounts"
-# fasuser = "elections"
-# faspw = "elections"
-# fas2_url = 'http://fakefas.fedoraproject.org/accounts/'
-fas2_url = app.config['FAS_BASE_URL']
-fas2_username = app.config['FAS_USERNAME']
-fas2_password = app.config['FAS_PASSWORD']
-fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password,
- insecure=True)
-
-
-# modular imports
-from fedora_elections import forms, models, redirect
-
-
-def remove_csrf(form_data):
- return dict([(k, v) for k, v in form_data.items() if k != 'csrf'])
-
-
-def login_required(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- if flask.g.fas_user is None:
- return flask.redirect(flask.url_for('auth_login',
- next=flask.request.url))
- return f(*args, **kwargs)
- return decorated_function
-
-
-def election_admin_required(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- if flask.g.fas_user is None:
- return flask.redirect(flask.url_for('auth_login',
- next=flask.request.url))
- if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
- flask.g.fas_user.groups:
- flask.abort(403)
- return f(*args, **kwargs)
- return decorated_function
-
-
-### LIST VIEWS #############################################
-
-(a)app.route('/')
-def index():
- elections = models.Election.query.filter_by(frontpage=True).all()
- num_elections = len(elections)
- return flask.render_template('list/index.html', elections=elections,
- num_elections=num_elections,
- title="Elections")
-
-
-(a)app.route('/about/<election_alias>')
-def about_election(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template('election/about.html', election=election,
- usernamemap=usernamemap)
-
-
-(a)app.route('/archive')
-def archived_elections():
- now = datetime.utcnow()
-
- try:
- elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date<now, models.Election.id>0).all()
- except NoResultFound:
- flask.flash('There are no archived elections.')
- return redirect.safe_redirect_back()
-
- num_elections = len(elections)
- if (num_elections == 0):
- flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
-
- return flask.render_template('election/archive.html',
- num_elections=num_elections,
- elections=elections)
-
-
-(a)app.route('/open')
-def open_elections():
- now = datetime.utcnow()
-
- try:
- elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date>now,
- models.Election.id>0).all()
- except NoResultFound:
- flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
-
- num_elections = models.Election.query.order_by(models.Election.start_date).\
- filter(models.Election.end_date>now,
- models.Election.id>0).count()
- if (num_elections == 0):
- flask.flash('There are no open elections.')
- return redirect.safe_redirect_back()
-
- return flask.render_template('list/index.html', elections=elections,
- num_elections=num_elections,
- title='Open Elections')
-
-
-### ELECTION VIEWS #############################################
-
-(a)app.route('/vote/<election_alias>', methods=['GET', 'POST'])
-def vote(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
-
- if election.status == 'Pending':
- flask.flash('Voting has not yet started, sorry.')
- return redirect.safe_redirect_back()
-
- elif election.status == 'Ended':
- flask.flash('This election is closed. You have been redirected to ' +
- 'the election results.')
- return flask.redirect(flask.url_for('results', election.shortname))
-
- votes = models.Vote.query.filter_by(election_id=election.id,
- voter=flask.g.fas_user.username).count()
- if (votes != 0):
- flask.flash('You have already voted in the election!')
- return redirect.safe_redirect_back()
-
- num_candidates = election.candidates.count()
-
- uvotes = {}
- for candidate in election.candidates:
- uvotes[candidate.id] = ""
- next_action = 'vote'
-
- if flask.request.method == 'POST':
- next_action = ''
- form_values = flask.request.form.values()
- if 'Submit' in form_values:
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote >= 0 and vote <= num_candidates:
- uvotes[candidate.id] = int(vote)
- else:
- flask.flash("Invalid Ballot!")
- return redirect.safe_redirect_back()
-
- except ValueError:
- flask.flash("Invalid Ballot!")
- return redirect.safe_redirect_back()
-
- for uvote in uvotes:
- new_vote = models.Vote(election_id=election.id,
- voter=flask.g.fas_user.username,
- timestamp=datetime.now(),
- candidate_id=uvote,
- value=uvotes[uvote] )
- db.session.add(new_vote)
- db.session.commit()
-
- flask.flash("Your vote has been recorded. Thank you!")
- return redirect.safe_redirect_back()
-
- elif 'Preview' in form_values:
- flask.flash("Please confirm your vote!")
- for candidate in election.candidates:
- try:
- vote = int(flask.request.form[str(candidate.id)])
- if vote > num_candidates:
- flask.flash("Invalid data.")
- uvotes[candidate.id] = 0
- next_action = 'vote'
- elif vote >= 0:
- uvotes[candidate.id] = vote
- else:
- flask.flash("Invalid data2.")
- uvotes[c.id] = 0
- next_action = 'vote'
- except ValueError:
- flask.flash("Invalid data")
-
- if (next_action != 'vote'):
- next_action = 'confirm'
-
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template('election/vote.html', election=election,
- num_candidates=num_candidates,
- usernamemap=usernamemap,
- voteinfo=uvotes, nextaction=next_action)
-
-
-### AUTH VIEWS #############################################
-
-(a)app.route('/login', methods=('GET', 'POST'))
-def auth_login():
- if flask.g.fas_user:
- return redirect.safe_redirect_back()
- form = forms.LoginForm()
- if form.validate_on_submit():
- if fas.login(form.username.data, form.password.data):
- flask.flash('Welcome, %s' % flask.g.fas_user.username)
- return redirect.safe_redirect_back()
- else:
- flask.flash('Incorrect username or password')
- return flask.render_template('auth/login.html', form=form)
-
-
-(a)app.route('/logout')
-def auth_logout():
- if not flask.g.fas_user:
- return redirect.safe_redirect_back()
- fas.logout()
- flask.flash('You have been logged out')
- return redirect.safe_redirect_back()
-
-
-### ELECTION ADMIN VIEWS ###################################
-
-(a)app.route('/admin/')
-@election_admin_required
-def admin_view_elections():
- elections = models.Election.query.\
- filter_by(fas_user=flask.g.fas_user.username).all()
- num_elections = len(elections)
- if (num_elections == 0):
- flask.flash('You do not have any elections.')
- return redirect.safe_redirect_back()
-
- return flask.render_template('admin/all_elections.html',
- num_elections=num_elections,
- elections=elections)
-
-
-(a)app.route('/admin/new', methods=("GET", "POST"))
-@election_admin_required
-def admin_new_election():
- form = forms.ElectionForm()
- if form.validate_on_submit():
- election = models.Election(**remove_csrf(form.data))
- # Fix start_date and end_date to use datetime
- election.start_date = datetime.combine(election.start_date, time())
- election.end_date = datetime.combine(election.end_date,
- time(23, 59, 59))
- election.fas_user = flask.g.fas_user.username
- db.session.add(election)
- admin = models.ElectionAdminGroup(election=election,
- group_name=app.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
- role_required=u'user')
- db.session.add(admin)
- db.session.commit()
- flask.flash('Election "%s" added' % election.summary)
- fedmsgshim.publish(topic="election.new", msg=election)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/election_form.html', form=form,
- submit_text='Create election')
-
-
-(a)app.route('/admin/<election_alias>/')
-@election_admin_required
-def admin_view_election(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
- if not election:
- flask.abort(404)
- return flask.render_template('admin/view_election.html', election=election)
-
-
-(a)app.route('/admin/<election_alias>/edit', methods=("GET", "POST"))
-@election_admin_required
-def admin_edit_election(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
- if not election:
- flask.abort(404)
- form = forms.ElectionForm(election.id, obj=election)
- if form.validate_on_submit():
- form.populate_obj(election)
- db.session.commit()
- flask.flash('Election "%s" saved' % election.summary)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/election_form.html', form=form,
- submit_text='Edit election')
-
-
-(a)app.route('/admin/<election_alias>/candidates/new', methods=("GET", "POST"))
-@election_admin_required
-def admin_add_candidate(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).first()
- if not election:
- flask.abort(404)
- form = forms.CandidateForm()
- if form.validate_on_submit():
- candidate = models.Candidate(election=election,
- **remove_csrf(form.data))
- db.session.add(candidate)
- db.session.commit()
- flask.flash('Candidate "%s" saved' % candidate.name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/candidate.html', form=form,
- submit_text='Add candidate')
-
-
-(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
- methods=("GET", "POST"))
-@election_admin_required
-def admin_edit_candidate(election_alias, candidate_id):
- election = models.Election.query.filter_by(alias=election_alias).first()
- candidate = models.Candidate.query.filter_by(id=candidate_id).first()
- if not election or not candidate:
- flask.abort(404)
- if candidate.election != election:
- flask.abort(404)
- form = forms.CandidateForm(obj=candidate)
- if form.validate_on_submit():
- form.populate_obj(candidate)
- db.session.commit()
- flask.flash('Candidate "%s" saved' % candidate.name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/candidate.html', form=form,
- submit_text='Edit candidate')
-
-
-(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
- methods=("GET", "POST"))
-@election_admin_required
-def admin_delete_candidate(election_alias, candidate_id):
- election = models.Election.query.filter_by(alias=election_alias).first()
- candidate = models.Candidate.query.filter_by(id=candidate_id).first()
- if not election or not candidate:
- flask.abort(404)
- if candidate.election != election:
- flask.abort(404)
- form = forms.ConfirmationForm()
- if form.validate_on_submit():
- db.session.delete(candidate)
- candidate_name = candidate.name
- db.session.commit()
- flask.flash('Candidate "%s" deleted' % candidate_name)
- return flask.redirect(flask.url_for('admin_view_election',
- election_alias=election.alias))
- return flask.render_template('admin/delete_candidate.html', form=form,
- candidate=candidate)
-
-
-(a)app.route('/results/<election_alias>')
-def election_results(election_alias):
- try:
- election = models.Election.query.filter_by(alias=election_alias).one()
- except NoResultFound:
- flask.flash('The election, %s, does not exist.' % election_alias)
- return redirect.safe_redirect_back()
-
- if election.status == 'In progress':
- flask.flash("We are sorry. The results for this election cannot be" \
- " viewed at this time because the election is still in" \
- " progress.")
- return redirect.safe_redirect_back()
-
- elif election.status == 'Pending':
- flask.flash("We are sorry. The results for this election cannot be" \
- " viewed at this time because the election has not" \
- " started.")
- return redirect.safe_redirect_back()
-
- elif election.embargoed == 1:
- if flask.g.fas_user is None:
- flask.flash("We are sorry. The results for this election" \
- "cannot be viewed because they are currently" \
- " embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
- else:
- if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
- flask.g.fas_user.groups:
- pass
- else:
- match = 0
- admingroups = models.ElectionAdminGroup.query.filter_by(
- election_id=election.id).all()
- for admingroup in admingroups:
- if admingroup.group_name in flask.g.fas_user.groups:
- match = 1
-
- if match == 0:
- flask.flash("We are sorry. The results for this election" \
- "cannot be viewed because they are currently" \
- " embargoed pending formal announcement.")
- return redirect.safe_redirect_back()
-
- usernamemap = {}
- if (election.candidates_are_fasusers):
- for candidate in election.candidates:
- try:
- usernamemap[candidate.id] = \
- fas2.person_by_username(candidate.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist.
- usernamemap[candidate.id] = candidate.name
-
- return flask.render_template('election/results.html', election=election,
- usernamemap=usernamemap)
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
deleted file mode 100644
index 7b5b794..0000000
--- a/fedora_elections/default_config.py
+++ /dev/null
@@ -1 +0,0 @@
-FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
diff --git a/fedora_elections/fedmsgshim.py b/fedora_elections/fedmsgshim.py
deleted file mode 100644
index 775f785..0000000
--- a/fedora_elections/fedmsgshim.py
+++ /dev/null
@@ -1,17 +0,0 @@
-""" This is a temporary shim that allows fedmsg to be an optional dependency of
-elections. If fedmsg is installed, these function calls will try to actually send
-messages. If it is not installed, it will return silently.
-
- :Author: Ralph Bean <rbean(a)redhat.com>
-
-"""
-
-import warnings
-
-
-def publish(*args, **kwargs):
- try:
- import fedmsg
- fedmsg.publish(*args, **kwargs)
- except Exception, e:
- warnings.warn(str(e))
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
deleted file mode 100644
index f050dbd..0000000
--- a/fedora_elections/forms.py
+++ /dev/null
@@ -1,69 +0,0 @@
-from flask.ext import wtf
-
-from fedora_elections.models import Election
-
-
-class ElectionForm(wtf.Form):
- summary = wtf.TextField('Summary', [wtf.validators.Required(),
- wtf.validators.Length(max=150)])
- alias = wtf.TextField('Alias', [
- wtf.validators.Required(),
- wtf.validators.Length(max=100),
- wtf.validators.Regexp('[a-z0-9_-]+',
- message=('Alias may only contain lower case '
- 'letters, numbers, hyphens and '
- 'underscores.')),
- ])
- description = wtf.TextAreaField('Description', [wtf.validators.Required()])
- url = wtf.TextField('URL', [wtf.validators.Required(),
- wtf.validators.URL(),
- wtf.validators.Length(max=250)])
- start_date = wtf.DateField('Start date', [wtf.validators.Required()])
- end_date = wtf.DateField('End date', [wtf.validators.Required()])
- number_elected = wtf.IntegerField('Number elected',
- [wtf.validators.Required(),
- wtf.validators.NumberRange(min=1)],
- default=1)
- candidates_are_fasusers = wtf.BooleanField('Candidates are FAS users?')
- frontpage = wtf.BooleanField('Show on front page?')
- embargoed = wtf.BooleanField('Embargo results?', default=True)
-
- def __init__(form, election_id=None, *args, **kwargs):
- super(ElectionForm, form).__init__(*args, **kwargs)
- form._election_id = election_id
-
- def validate_summary(form, field):
- check = Election.query.filter_by(summary=form.summary.data).all()
- if check:
- if not (form._election_id and form._election_id == check[0].id):
- raise wtf.ValidationError('There is already another election '
- 'with this summary.')
-
- def validate_alias(form, field):
- if form.alias.data == 'new':
- raise wtf.ValidationError(flask.Markup('The alias cannot be '
- '<code>new</code>.'))
- check = Election.query.filter_by(alias=form.alias.data).all()
- if check:
- if not (form._election_id and form._election_id == check[0].id):
- raise wtf.ValidationError('There is already another election '
- 'with this alias.')
-
- def validate_end_date(form, field):
- if form.end_date.data <= form.start_date.data:
- raise wtf.ValidationError('End date must be later than '
- 'start date.')
-
-
-class CandidateForm(wtf.Form):
- name = wtf.TextField('Name', [wtf.validators.Required()])
- url = wtf.TextField('URL', [wtf.validators.Length(max=250)])
-
-
-class LoginForm(wtf.Form):
- username = wtf.TextField('Username', [wtf.validators.Required()])
- password = wtf.PasswordField('Password', [wtf.validators.Required()])
-
-
-class ConfirmationForm(wtf.Form):
- pass
diff --git a/fedora_elections/images/border-left.png b/fedora_elections/images/border-left.png
deleted file mode 100644
index 6f256df..0000000
Binary files a/fedora_elections/images/border-left.png and /dev/null differ
diff --git a/fedora_elections/images/border-right.png b/fedora_elections/images/border-right.png
deleted file mode 100644
index 0e7365b..0000000
Binary files a/fedora_elections/images/border-right.png and /dev/null differ
diff --git a/fedora_elections/images/fedora-logo.png b/fedora_elections/images/fedora-logo.png
deleted file mode 100644
index ce95893..0000000
Binary files a/fedora_elections/images/fedora-logo.png and /dev/null differ
diff --git a/fedora_elections/images/line.png b/fedora_elections/images/line.png
deleted file mode 100644
index 3498cb9..0000000
Binary files a/fedora_elections/images/line.png and /dev/null differ
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
deleted file mode 100644
index b19bd33..0000000
--- a/fedora_elections/models.py
+++ /dev/null
@@ -1,103 +0,0 @@
-from fedora_elections import app, db
-
-from datetime import datetime
-
-class Election(db.Model):
- __tablename__ = 'elections'
-
- id = db.Column(db.Integer, primary_key=True)
- summary = db.Column(db.Unicode(150), unique=True, nullable=False)
- alias = db.Column(db.Unicode(100), unique=True, nullable=False)
- description = db.Column(db.UnicodeText, nullable=False)
- url = db.Column(db.Unicode(250), nullable=False)
- start_date = db.Column(db.DateTime, nullable=False)
- end_date = db.Column(db.DateTime, nullable=False)
- number_elected = db.Column(db.Integer, nullable=False, default=1)
- embargoed = db.Column(db.Boolean, nullable=False, default=True)
- frontpage = db.Column(db.Boolean, nullable=False, default=False)
- voting_type = db.Column(db.Unicode(100), nullable=False, default=u'range')
- candidates_are_fasusers = db.Column(db.Boolean, nullable=False,
- default=False)
- fas_user = db.Column(db.Unicode(50), nullable=False)
-
- @property
- def status(self):
- if datetime.now() < self.start_date:
- return 'Pending'
- else:
- if datetime.now() < self.end_date:
- return 'In progress'
- else:
- if self.embargoed:
- return 'Embargoed'
- else:
- return 'Ended'
-
- @property
- def locked(self):
- return datetime.now() >= self.start_date
-
-
-class ElectionAdminGroup(db.Model):
- __tablename__ = 'electionadmingroups'
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('admin_groups',
- lazy='dynamic'))
- group_name = db.Column(db.Unicode(150), nullable=False)
- role_required = db.Column(db.Enum(u'user', u'sponsor', u'administrator'),
- nullable=False)
-
-
-class Candidate(db.Model):
- __tablename__ = 'candidates'
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('candidates',
- lazy='dynamic'))
- name = db.Column(db.Unicode(150), nullable=False) # FAS username if
- # candidates_are_fasusers
- url = db.Column(db.Unicode(250))
-
- @property
- def vote_count(self):
- values = [v.value for v in self.votes]
- return sum(values)
-
-
-class LegalVoter(db.Model):
- __tablename__ = 'legalvoters'
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('legal_voters',
- lazy='dynamic'))
- # special names:
- # "cla + one" = cla_done + 1 non-cla group
- group_name = db.Column(db.Unicode(150), nullable=False)
-
-
-class Vote(db.Model):
- __tablename__ = 'votes'
-
- __table_args__ = (db.UniqueConstraint('election_id', 'voter',
- 'candidate_id',
- name='eid_voter_cid'), {} )
-
- id = db.Column(db.Integer, primary_key=True)
- election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
- nullable=False)
- election = db.relationship('Election', backref=db.backref('votes',
- lazy='dynamic'))
- voter = db.Column(db.Unicode(150), nullable=False)
- timestamp = db.Column(db.DateTime, nullable=False)
- candidate_id = db.Column(db.Integer, db.ForeignKey('candidates.id'),
- nullable=False)
- candidate = db.relationship('Candidate', backref=db.backref('votes',
- lazy='dynamic'))
- value = db.Column(db.Integer, nullable=False)
diff --git a/fedora_elections/redirect.py b/fedora_elections/redirect.py
deleted file mode 100644
index 5571abe..0000000
--- a/fedora_elections/redirect.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import flask
-
-from urlparse import urljoin, urlparse
-
-
-def is_safe_url(target):
- ref_url = urlparse(flask.request.host_url)
- test_url = urlparse(urljoin(flask.request.host_url, target))
- return test_url.scheme in ('http', 'https') and \
- ref_url.netloc == test_url.netloc
-
-
-def safe_redirect_back(next=None, fallback=('index', {})):
- targets = []
- if next:
- targets.append(next)
- if 'next' in flask.request.args and \
- flask.request.args['next']:
- targets.append(flask.request.args['next'])
- targets.append(flask.url_for(fallback[0], **fallback[1]))
- for target in targets:
- if is_safe_url(target):
- return flask.redirect(target)
diff --git a/fedora_elections/static/style.css b/fedora_elections/static/style.css
deleted file mode 100644
index 8c4a9b3..0000000
--- a/fedora_elections/static/style.css
+++ /dev/null
@@ -1,144 +0,0 @@
-body {
- font-family: sans-serif;
- font-size: 12pt;
- line-height: 1.35;
-}
-
-#flashes {
- background-color: #ffa;
-}
-
-nav {
- background-color: #eee;
- padding: 0.5em 1em;
-}
-
-nav h2 {
- display: inline;
- font-size: inherit;
- margin-right: 1em;
-}
-
-nav ul {
- display: inline;
- list-style: none;
- padding: 0;
-}
-
-nav li {
- display: inline;
- margin-right: .5em;
-}
-
-.clear {
- clear: both;
-}
-
-/* input elements */
-
-fieldset {
- border: 0;
- padding: 0;
- margin: .5em 0;
- float: left;
- clear: both;
-}
-
-.row {
- margin: .25em 0;
-}
-
-.row label:first-child {
- width: 8em;
- margin-right: 0.75em;
- text-align: right;
- clear: both;
- font-weight: bold;
- display: inline-block;
- vertical-align: top;
-}
-
-.row textarea {
- width: 30em;
- height: 8em;
-}
-
-form .row .errors {
- list-style: none;
- margin: .5em 0 1em 10.9375em;
- padding: 0;
- font-size: .8em;
- color: #c00;
- width: 30em;
-}
-
-div.help {
- width: 40%;
- float: right;
- background-color: #ffa;
- border-top: 1px solid #ee9;
- padding: 1em;
- font-size: .8em;
- margin: .625em 0;
-}
-
-div > *:first-child { margin-top: 0; }
-div > *:last-child { margin-bottom: 0; }
-
-form .buttons.indent {
- margin-left: 8.75em;
-}
-
-.vthot
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #FFFFFF;
- background: #9C0000;
-}
-
-.vtwarm
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #FFA300;
-}
-
-.vtmedwarm
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #93FF63;
-}
-
-.vtmedcool
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #00DAFE;
-}
-
-.vtcool
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #0033FF;
-}
-
-.vtcold
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #FFFFFF;
- background: #000083;
-}
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
deleted file mode 100644
index ff781b7..0000000
--- a/fedora_elections/templates/_formhelpers.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% macro render_field(field, after="") %}
-<div class="row">
- {{ field.label }}{{ field(**kwargs)|safe }}{% if after %} {{ after }}{% endif %}
- {% if field.errors %}
- <ul class="errors">
- {% for error in field.errors %}
- <li>{{ error }}</li>
- {% endfor %}
- </ul>
- {% endif %}
-</div>
-{% endmacro %}
-{# vim: set ts=2 sw=2 sta et: #}
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
deleted file mode 100644
index e168ac0..0000000
--- a/fedora_elections/templates/admin/all_elections.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% extends "base.html" %}
-{% block title %}Election administration{% endblock %}
-{% block body %}
- {% if num_elections > 0 %}
- <h2>Election administration</h2>
- <table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>
- <a href="{{ url_for('admin_view_election',
- election_alias=election.alias) }}">
- {{ election.summary }}
- </a>
- </td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- </tr>
- {% endfor %}
- </table>
- {% endif %}
-{%endblock %}
diff --git a/fedora_elections/templates/admin/candidate.html b/fedora_elections/templates/admin/candidate.html
deleted file mode 100644
index a7b2fda..0000000
--- a/fedora_elections/templates/admin/candidate.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}{{ submit_text }}{% endblock %}
-{% block body %}
- <h2>{{ submit_text }}</h2>
-
- <form action="" method="post">
- <fieldset>
- {{ render_field(form.name) }}
- {{ render_field(form.url) }}
- </fieldset>
-
- <br class="clear">
- <p class="buttons">
- <input type="submit" class="submit positive" value="{{ submit_text }}">
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora_elections/templates/admin/delete_candidate.html b/fedora_elections/templates/admin/delete_candidate.html
deleted file mode 100644
index d764f19..0000000
--- a/fedora_elections/templates/admin/delete_candidate.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}Delete candidate{% endblock %}
-{% block body %}
- <h2>{{ self.title() }}</h2>
-
- <form action="" method="post">
- <p>Are you sure you want to delete candidate "{{ candidate.name }}"?</p>
- <p class="buttons">
- <input type="submit" value="Delete candidate">
- <a href="{{ url_for('admin_view_election',
- election_alias=candidate.election.alias) }}">
- Cancel
- </a>
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
deleted file mode 100644
index 6e2a3d6..0000000
--- a/fedora_elections/templates/admin/election_form.html
+++ /dev/null
@@ -1,77 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}{{ submit_text }}{% endblock %}
-{% block body %}
- <h2>{{ self.title() }}</h2>
-
- <form action="" method="post">
- <fieldset>
- {{ render_field(form.summary) }}
- </fieldset>
- <div class="help">
- <p>This is the name of the election that users will first see.</p>
- <p>Example: <kbd>Fedora Board, November 2010</kbd></p>
- </div>
- <fieldset>
- {{ render_field(form.alias) }}
- </fieldset>
- <div class="help">
- <p>
- A short, unique name that will be used for the election's URL.
- Lowercase letters, numbers, hyphens and underscores only.
- </p>
- <p>Example: <kbd>boardf15</kbd></p>
- </div>
- <fieldset>
- {{ render_field(form.description) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.url) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.start_date, " at 00:00:00 UTC") }}
- {{ render_field(form.end_date, " at 23:59:59 UTC") }}
- </fieldset>
- <div class="help">
- <p>Enter dates in <kbd>YYYY-MM-DD</kbd> format.</p>
- </div>
- <fieldset>
- {{ render_field(form.number_elected) }}
- </fieldset>
- <div class="help">
- <p>
- This determines the cutoff for how many candidates win the election.
- For example, if you are electing four empty seats on a committee,
- enter <kbd>4</kbd> here.
- </p>
- </div>
- <fieldset>
- {{ render_field(form.candidates_are_fasusers) }}
- </fieldset>
- <div class="help">
- <p>
- Check this if every candidate in this election has a FAS user account.
- This will automatically pull full names and wiki page URLs from FAS.
- </p>
- </div>
- <fieldset>
- {{ render_field(form.frontpage) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.embargoed) }}
- </fieldset>
- <div class="help">
- <p>
- Embargo results when the election is finished. To unembargo them, edit
- the election and uncheck this box.
- </p>
- </div>
-
- <br class="clear">
- {% block election_form_extra %}{% endblock %}
- <p class="buttons">
- <input type="submit" class="submit positive" value="Save Changes">
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
deleted file mode 100644
index ed070f3..0000000
--- a/fedora_elections/templates/admin/view_election.html
+++ /dev/null
@@ -1,42 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- <h2>{{ election.summary }}</h2>
- <p>{{ election.description }}</p>
- <p>
- <a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">
- Edit election
- </a>
- </p>
- <ul>
- <li>On front page: {{ election.frontpage }}</li>
- <li>Start date: {{ election.start_date }}</li>
- <li>End date: {{ election.end_date }}</li>
- <li>Number elected: {{ election.number_elected }}</li>
- <li>Election type: {{ election.voting_type }}</li>
- </ul>
- <h3>Candidates</h3>
- {% if election.candidates.all() %}
- <ul>
- {% for candidate in election.candidates.all() %}
- <li>{{ candidate.name }}
- <a href="{{ url_for('admin_edit_candidate',
- election_alias=election.alias,
- candidate_id=candidate.id) }}">
- [edit]
- </a>
- <a href="{{ url_for('admin_delete_candidate',
- election_alias=election.alias,
- candidate_id=candidate.id) }}">
- [x]
- </a>
- </li>
- {% endfor %}
- </ul>
- {% else %}
- <p>There are no candidates.</p>
- {% endif %}
- <a href="{{ url_for('admin_add_candidate',
- election_alias=election.alias) }}">
- Add a candidate
- </a>
-{% endblock %}
diff --git a/fedora_elections/templates/auth/login.html b/fedora_elections/templates/auth/login.html
deleted file mode 100644
index 9e7b73c..0000000
--- a/fedora_elections/templates/auth/login.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "base.html" %}
-{% from "_formhelpers.html" import render_field %}
-{% block title %}Log in{% endblock %}
-{% block body %}
- <h2>Log in</h2>
-
- <form action="" method="post">
- <p>
- Log in using your
- <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a>
- account.
- </p>
- {{ render_field(form.username) }}
- {{ render_field(form.password) }}
- <p class="buttons indent"><input type="submit" class="submit positive"
- value="Log in">
- </p>
- {{ form.csrf_token }}
- </form>
-{% endblock %}
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
deleted file mode 100644
index 3e6f806..0000000
--- a/fedora_elections/templates/base.html
+++ /dev/null
@@ -1,134 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- {% block head %}
- <title>
- Fedora Elections
- {% if self.title() %}
- —
- {% block title %}{% endblock %}
- {% endif %}
- </title>
- <link rel="stylesheet" type="text/css" media="screen"
- href="{{ url_for("static", filename="style.css") }}" />
- <link rel="stylesheet" type="text/css" media="screen"
- href="https://fedoraproject.org/static/css/fedora.css" />
- {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
- {% endfor %}
- {% endblock %}
- </head>
- <body>
- <div id="wrapper">
- <div id="head">
- <h1><a href="https://admin.fedoraproject.org/en/index">Fedora</a></h1>
- </div>
-
- <div id="sidebar">
- <div id="nav">
- <h2>Navigation</h2>
- <ul>
- <li id="nav-home">
- <a href="http://fedoraproject.org/en/index">Home</a>
- </li>
- <li id="nav-get">
- <a href="http://fedoraproject.org/en/get-fedora">Get Fedora</a>
- </li>
- <li id="nav-join">
- <a href="http://fedoraproject.org/en/join-fedora">Join Fedora</a>
- </li>
- <li id="nav-help">
- <a href="http://fedoraproject.org/en/get-help">Get Help</a>
- </li>
- </ul>
- </div>
- </div>
-
- <div id="content">
- <ul class="toolbar" id="#main-toolbar">
- {% if g.fas_user %}
- <li>
- Hello, {{ g.fas_user.username }} —
- <a href="{{ url_for("auth_logout") }}">log out</a>
- </li>
- {% else %}
- <li>
- You are not logged in —
- <a href="{{ url_for("auth_login") }}">log in</a>
- </li>
- {% endif %}
- </ul>
- <h2>Elections</h2>
- <ul>
- <li>
- <a href="{{ url_for("open_elections") }}">
- Open elections
- </a>
- </li>
- <li>
- <a href="{{ url_for("archived_elections") }}">
- Archived elections
- </a>
- </li>
- </ul>
- <br>
- <h2>Administration</h2>
- <ul>
- <li>
- <a href="{{ url_for("admin_view_elections") }}">
- My elections
- </a>
- </li>
- <li>
- <a href="{{ url_for("admin_new_election") }}">
- Create new election
- </a>
- </li>
- </ul>
- {% with messages = get_flashed_messages() %}
- {% if messages %}
- <ul id="flashes">
- {% for message in messages %}
- <li>{{ message }}</li>
- {% endfor %}
- </ul>
- {% endif %}
- {% endwith %}
- {% block body %}
- {% endblock %}
- </div>
- </div>
- <div id="bottom">
- <div id="footer">
- {% block footer %}
- <p class="copy">
- Copyright © 2008 Red Hat, Inc. and others. Please send any
- comments or corrections to the
- <a href="mailto:webmaster@fedoraproject.org">
- websites team
- </a>.
- </p>
- <p class="disclaimer">
- The Fedora Project is maintained and driven by the community and
- sponsored by Red Hat. This is a community maintained site. Red
- Hat is not responsible for content.
- </p>
- <ul>
- <li class="first">
- <a href="/en/sponsors">Sponsors</a>
- </li>
- <li>
- <a href="http://fedoraproject.org/wiki/Legal">
- Legal
- </a>
- </li>
- <li>
- <a href="http://fedoraproject.org/wiki/Legal/TrademarkGuidelines">
- Trademark Guidelines
- </a>
- </li>
- </ul>
- </div>
- {% endblock %}
- </div>
- </body>
-</html>
diff --git a/fedora_elections/templates/election/about.html b/fedora_elections/templates/election/about.html
deleted file mode 100644
index c7bf0ec..0000000
--- a/fedora_elections/templates/election/about.html
+++ /dev/null
@@ -1,40 +0,0 @@
-{% extends "base.html" %}
-{% block title %}Election Information{% endblock %}
-{% block body %}
- <h1>{{ election.summary }}</h1>
- <p>{{ election.description }}</p>
- {% if election.url %}
- <p><a href="{{ election.url }}">[More Information]</a></p>
- {% endif %}
- {% if election.status == 'Ended' %}
- <a href="/results/{{ election.alias }}">Show Results!</a>
- {% endif %}
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- {% if candidate.url %}
- <small><a href="{{ candidate.url }}">[info]</a></small>
- {% endif %}
- </td>
- </tr>
- {% endfor %}
- </table>
- {% if election.legal_voters.count() != 0 %}
- <p>
- To vote in this election you must be a member of any one of the following groups:
- </p>
- <ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
- </ul>
- {% else %}
- <p />
- {% endif %}
-{% endblock %}
diff --git a/fedora_elections/templates/election/archive.html b/fedora_elections/templates/election/archive.html
deleted file mode 100644
index 8740b3c..0000000
--- a/fedora_elections/templates/election/archive.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- {% if num_elections > 0 %}
- <h2>Past Elections</h2>
- <table border="1" cellpadding="1">
- <tr>
- <th></th>
- <th>Start Date (UTC)</th>
- <th>End Date (UTC)</th>
- <th></th>
- <th></th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>{{election.description}}</td>
- <td>{{election.start_date}}</td>
- <td>{{election.end_date}}</td>
- <td>
- <a href="about/{{ election.alias }}"">More information</a>
- </td>
- <td>
- <a href="results/{{ election.alias }}">Results</a>
- </td>
- </tr>
- {% endfor %}
- </table>
- {% endif %}
-{% endblock %}
diff --git a/fedora_elections/templates/election/results.html b/fedora_elections/templates/election/results.html
deleted file mode 100644
index f61d214..0000000
--- a/fedora_elections/templates/election/results.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- <h1>{{ election.summary }} Results</h1>
- <p>{{ election.description }}</p>
- {% if election.url %}
- <p><a href="{{ election.url }}">[More Information]</a></p>
- {% endif %}
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{candidate.name}}
- {% endif %}
- {% if candidate.url %}
- <small><a href="{{ candidate.url }}">[info]</a></small>
- {% endif %}
- </td>
- <td>{{ candidate.vote_count }}</td>
- </tr>
- {% endfor %}
- </table>
-{% endblock %}
diff --git a/fedora_elections/templates/election/vote.html b/fedora_elections/templates/election/vote.html
deleted file mode 100644
index e3c4dd4..0000000
--- a/fedora_elections/templates/election/vote.html
+++ /dev/null
@@ -1,116 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-<h2>{{election.summary}}</h2>
-
-<p>{{election.description}}</p>
-
-<p><a href="{{election.url}}">[More Information]</a></p>
-
-<form action="" method="post">
- <table border="1" cellpadding="1">
- {% for candidate in election.candidates %}
- <tr>
- <td>
- {% if election.candidates_are_fasusers %}
- {{ usernamemap[candidate.id] }}
- {% else %}
- {{ candidate.name }}
- {% endif %}
- <a href="{{candidate.url}}">[Info]</a>
- </td>
- <!-- If user is logged in: -->
- {% if g.fas_user %}
- {% if nextaction == 'vote' %}
- <td>
- <select name="{{candidate.id}}">
- {% for opt in range(0, num_candidates+1) %}
- <option value={{opt}}>{{opt}}</option>
- {% endfor %}
- </select>
- </td>
- {% endif %}
- {% if nextaction == 'confirm' %}
- <td>
- {% set cid = candidate.id %}
- <input type="hidden" name="{{candidate.id}}"
- value="{{voteinfo[cid]}}" />
- {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
- {% if weightpercent >= 90 %}
- <div class="vthot">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 70 %}
- <div class="vtwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 50 %}
- <div class="vtmedwarm">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 30 %}
- <div class="vtmedcool">
- {{voteinfo[cid]}}
- </div>
- {% elif weightpercent >= 10 %}
- <div class="vtcool">
- {{voteinfo[cid]}}
- </div>
- {% else %}
- <div class="vtcold">
- {{voteinfo[cid]}}
- </div>
- {% endif %}
- </td>
- {% endif %}
- {% endif %}
- <!-- End if user logged in -->
- </tr>
- {% endfor %}
- </table>
- {% if g.fas_user %}
- <p>
- {% if nextaction == 'confirm' %}
- <input type="submit" name="{{nextaction}}" value="Submit" />
- {% else %}
- <input type="submit" name="{{nextaction}}" value="Preview" />
- {% endif %}
- </p>
- {% endif %}
-</form>
-
-<!-- Make this conditional on whether the user is logged in and can vote in the election -->
-{% if election.legal_voters.count() != 0 %}
-<p>
- To vote in this election you must be a member of any one of the following groups:
-</p>
-<ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
-</ul>
-{% else %}
-<p />
-{% endif %}
-<div>
- <h2>Information on Range Voting</h2>
- <p>
- Fedora Project has implemented
- <a href="http://rangevoting.org/">Range Voting</a>
- for this election, in particular the "Range (score-summing, blanks
- treated as zero score, no quorum rule)" range voting system.
- </p>
- <p>
- To cast your vote in this election simply select a value between 0 and
- {{num_candidates}} with 0 as 'least or no preference' and
- {{num_candidates}} as 'highest preference'.
- </p>
- <p>
- At the end of the election, the highest ranking candidate(s) are marked
- as the winners.
- </p>
- <p>
- For more information about Range Voting, visit the
- <a href="http://rangevoting.org/">Center for Range Voting</a>.
- </p>
-</div>
-{% endblock %}
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
deleted file mode 100644
index 290c2cf..0000000
--- a/fedora_elections/templates/list/index.html
+++ /dev/null
@@ -1,32 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
- {% if num_elections > 0 %}
- <h2>{{ title }}</h2>
- <table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- <th></th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>{{ election.summary }}</td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- <td>
- {% if election.status == "In progress" %}
- {% if g.fas_user %}
- <a href="{{ url_for('vote', election_alias=election.alias) }}">
- Vote now!
- </a>
- {% endif %}
- {% endif %}
- </td>
- </tr>
- {% endfor %}
- </table>
- {% endif %}
-{% endblock %}
diff --git a/httpd-fedora-elections.conf.in b/httpd-fedora-elections.conf.in
deleted file mode 100644
index c974b88..0000000
--- a/httpd-fedora-elections.conf.in
+++ /dev/null
@@ -1,23 +0,0 @@
-Alias /fedora-elections/images @DATADIR@/fedora-elections/images
-Alias /fedora-elections/static @DATADIR@/fedora-elections/static
-
-WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
-WSGISocketPrefix run/wsgi
-WSGIRestrictStdout On
-WSGIRestrictSignal Off
-WSGIPythonOptimize 1
-
-WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora-elections/fedora-elections.wsgi
-
-<Location /fedora-elections>
- WSGIProcessGroup fedora-elections
- <IfModule mod_authz_core.c>
- # Apache 2.4
- Require all granted
- </IfModule>
- <IfModule !mod_authz_core.c>
- # Apache 2.2
- Order deny,allow
- Allow from all
- </IfModule>
-</Location>
commit 2c35023daa442f40fd70477304fcfed6b5324a48
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Aug 24 03:33:26 2013 +0000
Update instructions
diff --git a/README b/README
index 610e7d6..442ebb5 100644
--- a/README
+++ b/README
@@ -50,9 +50,11 @@ You can obtain the code from github:
Configure the application
-------------------------
-./configure.py --install-conf=`pwd` --install-data=`pwd` --install-sbin=`pwd`
+cd fedora-elections
+../configure.py --install-conf=${PWD%/*} --install-data=${PWD%/*} --shared-state=${PWD%/*}
-This will create fedora-elections.wsgi and http-fedora-elections.conf.
+This will create createdb, fedora-elections.cfg, fedora-elections.wsgi and
+http-fedora-elections.conf.
Modify fedora-elections.cfg to change FAS_PASSWORD to the correct password.
commit c20c7ac0bbd25566fc3d01f77a9c8a71703af627
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Thu Jul 25 03:20:53 2013 +0000
Add constraint to votes table
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index dde8951..b19bd33 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -85,6 +85,10 @@ class LegalVoter(db.Model):
class Vote(db.Model):
__tablename__ = 'votes'
+ __table_args__ = (db.UniqueConstraint('election_id', 'voter',
+ 'candidate_id',
+ name='eid_voter_cid'), {} )
+
id = db.Column(db.Integer, primary_key=True)
election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
nullable=False)
commit db842723b9b480acc63a74a243832333becd37e0
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Thu Jul 25 03:20:23 2013 +0000
Fix processing 'My Elections'
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 83217bf..86bfdc6 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -444,21 +444,28 @@ def election_results(election_alias):
return redirect.safe_redirect_back()
elif election.embargoed == 1:
- if identity.in_group(config.get('admingroup', 'elections')):
- pass
- else :
- match = 0
- admingroups = models.ElectionAdminGroups.query.filter_by(
- election_id=election.id).all()
- for group in admingroups:
- if identity.in_group(group.group_name):
- match = 1
-
- if match == 0:
+ if flask.g.fas_user is None:
flask.flash("We are sorry. The results for this election" \
"cannot be viewed because they are currently" \
" embargoed pending formal announcement.")
return redirect.safe_redirect_back()
+ else:
+ if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] in \
+ flask.g.fas_user.groups:
+ pass
+ else:
+ match = 0
+ admingroups = models.ElectionAdminGroup.query.filter_by(
+ election_id=election.id).all()
+ for admingroup in admingroups:
+ if admingroup.group_name in flask.g.fas_user.groups:
+ match = 1
+
+ if match == 0:
+ flask.flash("We are sorry. The results for this election" \
+ "cannot be viewed because they are currently" \
+ " embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
usernamemap = {}
if (election.candidates_are_fasusers):
commit eb0da125e43b81887abbd36507462897a7c3c4e6
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jul 6 03:14:04 2013 +0000
Add task
diff --git a/TODO b/TODO
index ebace48..9573c62 100644
--- a/TODO
+++ b/TODO
@@ -6,6 +6,7 @@ Things that need doing to get this deployed
* rpmify
* rpmify flask-fas (or get flask-fas into python-fedora)
* Implement a voting method for range voting
+* Implement a voting method for non-range voting
* CSRF protection -- anything going through wtf forms is protected. It's up to
us to make sure that no data changing requests are coded without wtf forms.
commit 75f057138f95d253622263eecb4e42ac534e27ea
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jul 6 03:13:25 2013 +0000
Fix db issues
diff --git a/fedora-elections.spec b/fedora-elections.spec
index a130b53..61a39ac 100644
--- a/fedora-elections.spec
+++ b/fedora-elections.spec
@@ -127,7 +127,8 @@ rm -rf $RPM_BUILD_ROOT
%defattr(-,root,root,-)
%doc
%dir %{_sysconfdir}/%{name}
-%dir %{_sharedstatedir}/%{name}/
+%dir %{_sharedstatedir}/%{name}
+%attr(744,apache,apache) %{_sharedstatedir}/%{name}
%{_datadir}/%{name}/
%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
%config(noreplace) %{_sysconfdir}/httpd/conf.d/httpd-%{name}.conf
@@ -144,6 +145,10 @@ then
#
echo " Creating a database."
%{_datadir}/%{name}/createdb
+ echo " Changing permissions."
+ chmod 644 database.sqlite
+ echo " Changing owner."
+ chown apache:apache database.sqlite
fi
commit ddb4d65b9f393d9739a09594701f7be160c926d6
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jul 6 03:12:38 2013 +0000
Specify dummy FAS account
diff --git a/fedora-elections.cfg.in b/fedora-elections.cfg.in
index fef1c4a..ae68f66 100644
--- a/fedora-elections.cfg.in
+++ b/fedora-elections.cfg.in
@@ -8,8 +8,8 @@ SECRET_KEY = 'change me'
SQLALCHEMY_DATABASE_URI = 'sqlite:///@SHAREDSTDIR@/fedora-elections/database.sqlite'
FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
-FAS_USERNAME = 'elections'
-FAS_PASSWORD = 'elections_fas_password'
+FAS_USERNAME = 'fcelections'
+FAS_PASSWORD = 'el3cTo$hozer'
FAS_CHECK_CERT = False
FAS_HTTPS_REQUIRED = False
commit cba79d35f218d74dc45370e5138ae3c75e4bdd02
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jul 6 03:11:26 2013 +0000
Add elections owner to db
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 18cf36f..83217bf 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -135,7 +135,14 @@ def archived_elections():
flask.flash('There are no archived elections.')
return redirect.safe_redirect_back()
- return flask.render_template('election/archive.html', elections=elections)
+ num_elections = len(elections)
+ if (num_elections == 0):
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('election/archive.html',
+ num_elections=num_elections,
+ elections=elections)
@app.route('/open')
@@ -158,6 +165,7 @@ def open_elections():
return redirect.safe_redirect_back()
return flask.render_template('list/index.html', elections=elections,
+ num_elections=num_elections,
title='Open Elections')
@@ -290,8 +298,15 @@ def auth_logout():
@app.route('/admin/')
@election_admin_required
def admin_view_elections():
- elections = models.Election.query.filter_by().all()
+ elections = models.Election.query.\
+ filter_by(fas_user=flask.g.fas_user.username).all()
+ num_elections = len(elections)
+ if (num_elections == 0):
+ flask.flash('You do not have any elections.')
+ return redirect.safe_redirect_back()
+
return flask.render_template('admin/all_elections.html',
+ num_elections=num_elections,
elections=elections)
@@ -305,6 +320,7 @@ def admin_new_election():
election.start_date = datetime.combine(election.start_date, time())
election.end_date = datetime.combine(election.end_date,
time(23, 59, 59))
+ election.fas_user = flask.g.fas_user.username
db.session.add(election)
admin = models.ElectionAdminGroup(election=election,
group_name=app.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index 994f144..dde8951 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -18,6 +18,7 @@ class Election(db.Model):
voting_type = db.Column(db.Unicode(100), nullable=False, default=u'range')
candidates_are_fasusers = db.Column(db.Boolean, nullable=False,
default=False)
+ fas_user = db.Column(db.Unicode(50), nullable=False)
@property
def status(self):
commit f1ae6366a0fb99c9b3b22b31371b667abfcb6e4b
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jul 6 03:10:44 2013 +0000
Fix no elections issue
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index fafefee..e168ac0 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -1,26 +1,28 @@
{% extends "base.html" %}
{% block title %}Election administration{% endblock %}
{% block body %}
- <h2>Election administration</h2>
- <table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- </tr>
- {% for election in elections %}
+ {% if num_elections > 0 %}
+ <h2>Election administration</h2>
+ <table>
<tr>
- <td>
- <a href="{{ url_for('admin_view_election',
- election_alias=election.alias) }}">
- {{ election.summary }}
- </a>
- </td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
</tr>
- {% endfor %}
- </table>
+ {% for election in elections %}
+ <tr>
+ <td>
+ <a href="{{ url_for('admin_view_election',
+ election_alias=election.alias) }}">
+ {{ election.summary }}
+ </a>
+ </td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
{%endblock %}
diff --git a/fedora_elections/templates/election/archive.html b/fedora_elections/templates/election/archive.html
index ad35d6b..8740b3c 100644
--- a/fedora_elections/templates/election/archive.html
+++ b/fedora_elections/templates/election/archive.html
@@ -1,28 +1,28 @@
{% extends "base.html" %}
{% block body %}
-
- <h2>Past Elections</h2>
-
- <table border="1" cellpadding="1">
- <tr>
- <th></th>
- <th>Start Date (UTC)</th>
- <th>End Date (UTC)</th>
- <th></th>
- <th></th>
- </tr>
- {% for election in elections %}
+ {% if num_elections > 0 %}
+ <h2>Past Elections</h2>
+ <table border="1" cellpadding="1">
<tr>
- <td>{{election.description}}</td>
- <td>{{election.start_date}}</td>
- <td>{{election.end_date}}</td>
- <td>
- <a href="about/{{ election.alias }}"">More information</a>
- </td>
- <td>
- <a href="results/{{ election.alias }}">Results</a>
- </td>
+ <th></th>
+ <th>Start Date (UTC)</th>
+ <th>End Date (UTC)</th>
+ <th></th>
+ <th></th>
</tr>
- {% endfor %}
- </table>
+ {% for election in elections %}
+ <tr>
+ <td>{{election.description}}</td>
+ <td>{{election.start_date}}</td>
+ <td>{{election.end_date}}</td>
+ <td>
+ <a href="about/{{ election.alias }}"">More information</a>
+ </td>
+ <td>
+ <a href="results/{{ election.alias }}">Results</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
{% endblock %}
commit 2a2e646f66e905de6cfd89185b96a52cc6a8f403
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jun 15 03:56:56 2013 +0000
Changes to support building a good package
diff --git a/.gitignore b/.gitignore
index 7ecf5f6..ce498f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@ database.sqlite
*.pyc
*.pyo
*.swp
+createdb
development.cfg
fedora-elections.cfg
fedora-elections.wsgi
diff --git a/README b/README
index 69d11ea..610e7d6 100644
--- a/README
+++ b/README
@@ -54,6 +54,7 @@ Configure the application
This will create fedora-elections.wsgi and http-fedora-elections.conf.
+Modify fedora-elections.cfg to change FAS_PASSWORD to the correct password.
Create a database
-----------------
diff --git a/configure.py b/configure.py
index 9ea3f51..66d899f 100755
--- a/configure.py
+++ b/configure.py
@@ -35,23 +35,23 @@ parser.add_option('--install-conf', dest='confdir', action='store',
parser.add_option('--install-data', dest='datadir', action='store',
default='/usr/share',
help='Where to install data files.')
-parser.add_option('--install-sbindir', dest='sbindir', action='store',
- default='/usr/sbin',
- help='Where to install executeable files.')
+parser.add_option('--shared-state', dest='sharedstdir', action='store',
+ default='/var/lib',
+ help='Where to install shared state files.')
(options, args) = parser.parse_args()
-confdir = options.confdir
-datadir = options.datadir
-sbindir = options.sbindir
+confdir = options.confdir
+datadir = options.datadir
+sharedstdir = options.sharedstdir
-print " confdir = %s" % confdir
-print " datadir = %s" % datadir
-print " sbindir = %s" % sbindir
+print " confdir = %s" % confdir
+print " datadir = %s" % datadir
+print " sharedstdir = %s" % sharedstdir
substs = {'@CONFDIR@':confdir,
'@DATADIR@':datadir,
- '@SBINDIR@':sbindir
+ '@SHAREDSTDIR@':sharedstdir
}
pattern = re.compile("\.in$")
diff --git a/createdb b/createdb
index 4699a56..115635b 100755
--- a/createdb
+++ b/createdb
@@ -1 +1 @@
-FEDORA_ELECTIONS_CONFIG=../development.cfg python createdb.py
+FEDORA_ELECTIONS_CONFIG=/etc/fedora-elections/fedora-elections.cfg python createdb.py
diff --git a/createdb.in b/createdb.in
new file mode 100644
index 0000000..c7a87da
--- /dev/null
+++ b/createdb.in
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_CONFIG=@CONFDIR(a)/fedora-elections/fedora-elections.cfg python @DATADIR(a)/fedora-elections/createdb.py
diff --git a/createdb.py b/createdb.py
index e99a35c..a909c5e 100644
--- a/createdb.py
+++ b/createdb.py
@@ -1,2 +1,6 @@
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7']
+import pkg_resources
+
from fedora_elections import db
db.create_all()
diff --git a/fedora-elections.cfg.in b/fedora-elections.cfg.in
index fc0d50d..fef1c4a 100644
--- a/fedora-elections.cfg.in
+++ b/fedora-elections.cfg.in
@@ -5,11 +5,11 @@ DEBUG = True
# You will want to change this for your install
SECRET_KEY = 'change me'
-SQLALCHEMY_DATABASE_URI = 'sqlite:///@DATADIR@/database.sqlite'
+SQLALCHEMY_DATABASE_URI = 'sqlite:///@SHAREDSTDIR@/fedora-elections/database.sqlite'
-FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
+FAS_BASE_URL = 'https://admin.fedoraproject.org/accounts/'
FAS_USERNAME = 'elections'
-FAS_PASSWORD ='el3ctMEn0w'
+FAS_PASSWORD = 'elections_fas_password'
FAS_CHECK_CERT = False
FAS_HTTPS_REQUIRED = False
diff --git a/fedora-elections.spec b/fedora-elections.spec
index 7eff44d..a130b53 100644
--- a/fedora-elections.spec
+++ b/fedora-elections.spec
@@ -1,3 +1,8 @@
+%{!?pyver: %define pyver %(%{__python} -c "import sys ; print sys.version[:3]")}
+
+%define modname fedora_elections
+%define eggname fedora_elections
+
Name: fedora-elections
Version: 0.1
Release: 1%{?dist}
@@ -75,20 +80,43 @@ CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
%install
rm -rf $RPM_BUILD_ROOT
-%{__python} setup.py install -O1 --skip-build --install-data=%{_datadir} --root $RPM_BUILD_ROOT
+%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT
%{__mkdir_p} %{buildroot}%{_sysconfdir}/%{name}
-%{__mkdir_p} %{buildroot}%{_sysconfdir}/httpd/conf.d
-%{__mkdir_p} %{buildroot}%{_datadir}/%{name}
-%{__mkdir_p} %{buildroot}%{_datadir}/%{name}/static
+%{__install} -m 444 %{name}.cfg %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
-%{__install} -m 644 httpd-fedora-elections.conf \
+%{__mkdir_p} %{buildroot}%{_sysconfdir}/httpd/conf.d
+%{__install} -m 444 httpd-fedora-elections.conf \
%{buildroot}%{_sysconfdir}/httpd/conf.d/httpd-fedora-elections.conf
-%{__install} -m 644 fedora_elections/static/* %{buildroot}%{_datadir}/%{name}/static
-%{__install} -m 644 %{name}.cfg.sample %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
-%{__install} -m 644 createdb.py %{buildroot}%{_datadir}/%{name}/createdb.py
-%{__install} -m 644 %{name}.wsgi %{buildroot}%{_datadir}/%{name}/%{name}.wsgi
+%{__mkdir_p} %{buildroot}%{_datadir}/%{name}
+%{__install} -m 544 createdb %{buildroot}%{_datadir}/%{name}/createdb
+%{__install} -m 544 createdb.py %{buildroot}%{_datadir}/%{name}/createdb.py
+%{__install} -m 444 %{name}.wsgi %{buildroot}%{_datadir}/%{name}/%{name}.wsgi
+
+%{__mkdir_p} %{buildroot}%{_datadir}/%{name}/static
+%{__install} -m 444 fedora_elections/static/* %{buildroot}%{_datadir}/%{name}/static
+
+%{__mkdir_p} %{buildroot}%{_datadir}/%{name}/images
+%{__install} -m 444 fedora_elections/images/* %{buildroot}%{_datadir}/%{name}/images
+
+%{__mkdir_p} %{buildroot}%{_sharedstatedir}/%{name}
+
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/admin
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/auth
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/election
+%{__mkdir_p} %{buildroot}%{python_sitelib}/%{modname}/templates/list
+%{__install} -m 444 fedora_elections/templates/*.html \
+ %{buildroot}%{python_sitelib}/%{modname}/templates
+%{__install} -m 444 fedora_elections/templates/admin/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/admin
+%{__install} -m 444 fedora_elections/templates/auth/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/auth
+%{__install} -m 444 fedora_elections/templates/election/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/election
+%{__install} -m 444 fedora_elections/templates/list/* \
+ %{buildroot}%{python_sitelib}/%{modname}/templates/list
%clean
@@ -99,10 +127,42 @@ rm -rf $RPM_BUILD_ROOT
%defattr(-,root,root,-)
%doc
%dir %{_sysconfdir}/%{name}
+%dir %{_sharedstatedir}/%{name}/
+%{_datadir}/%{name}/
%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
%config(noreplace) %{_sysconfdir}/httpd/conf.d/httpd-%{name}.conf
-%{_datadir}/%{name}
-%{python_sitelib}/*
+%{python_sitelib}/%{modname}/
+%{python_sitelib}/%{eggname}-%{version}-py%{pyver}.egg-info/
+
+
+%post
+cd %{_sharedstatedir}/%{name}
+if [[ ! -f database.sqlite ]]
+then
+ #
+ # The database does not exist create one.
+ #
+ echo " Creating a database."
+ %{_datadir}/%{name}/createdb
+fi
+
+
+%postun
+cd %{_sharedstatedir}/%{name}
+if [[ -f database.sqlite ]]
+then
+ #
+ # Remove the database.
+ #
+ echo " Removing the database."
+ /bin/rm database.sqlite
+fi
+
+cd ${_sharedstatedir}
+if [[ -d %{_sharedstatedir}/%{name} ]]
+then
+ /bin/rmdir %{_sharedstatedir}/%{name}
+fi
%changelog
diff --git a/fedora-elections.wsgi.in b/fedora-elections.wsgi.in
index c7cfbb6..430cc5c 100644
--- a/fedora-elections.wsgi.in
+++ b/fedora-elections.wsgi.in
@@ -11,12 +11,11 @@ import sys
sys.stdout = sys.stderr
## Set the environment variable pointing to the configuration file
-os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections.cfg'
+os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections/fedora-elections.cfg'
## The following is only needed if you did not install
## as a python module (for example if you run it from a git clone).
import sys
# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
-sys.path.insert(0, '@SBINDIR@')
from fedora_elections import app as application
diff --git a/fedora_elections/static/fedora.css b/fedora_elections/static/fedora.css
deleted file mode 100644
index e56f85f..0000000
--- a/fedora_elections/static/fedora.css
+++ /dev/null
@@ -1,1334 +0,0 @@
-*
-{
- margin: 0;
- padding: 0;
-}
-
-html, body
-{
- height: 100%;
-}
-
-body
-{
- font-size: 76%;
- background: #FFFFFF url(../images/border-left.png) 0 0 repeat-y;
-}
-
-a img
-{
- border: none;
-}
-
-pre
-{
- font-size: 2.6ex;
- overflow: auto;
-}
-
-abbr, acronym {
- border-bottom: 1px dotted #000000;
- cursor: help;
-}
-
-#wrapper
-{
- font: normal 1em/1.5 sans-serif;
- margin-left: 18px;
- min-height: 100%;
- background: #FFFFFF url(../images/border-right.png) 100% 0 repeat-y;
- padding-right: 18px;
- overflow: hidden;
-}
-
-#head
-{
- position: relative;
- border-top: 10px solid #337ACC;
- padding: 1ex 2ex 17px;
- background: #FFFFFF url(../images/line.png) 0 100% repeat-x;
-}
-
-#head h1 a
-{
- display: block;
- text-indent: -9999px;
- background: url(../images/fedora-logo.png) 20px 50% no-repeat;
- height: 73px;
- width: 175px;
- overflow: hidden;
- text-decoration: none;
-}
-
-#sidebar
-{
- float: left;
- width: 180px;
- margin: 2ex 0;
- background: #FFFFFF;
- padding-bottom: 120px;
-}
-
-#banner
-{
- text-align: center;
-}
-
-#banner img
-{
- display: block;
- width: 200px;
- margin-left: 10px;
-}
-
-#nav
-{
- margin: 0 1.5ex;
- padding: 0 1.5ex;
-}
-
-#nav h2
-{
- font-size: 0.9em;
- border-bottom: 1px dotted #AAAAAA;
- color: #444444;
- margin: 2ex 0 1ex;
- text-transform: uppercase;
- font-weight: normal;
-}
-
-#nav ul
-{
- list-style: url(../images/arrow.png);
- margin-left: 15px;
-}
-
-#nav li
-{
- margin: 0.25ex 1ex;
- color: #777777;
- font-size: 0.9em;
-}
-
-#nav a
-{
- font-weight: bold;
- margin-right: 0.75ex;
- color: #729FCF;
- font-size: 1.1em;
- display: block;
-}
-
-.home #nav-home a, .get #nav-get a, .join #nav-join a, .help #nav-help a
-{
- color: #777777;
- text-decoration: none;
-}
-
-#content
-{
- display: inline-block;
- margin-left: 230px;
- padding: 1.5ex 2ex 140px 0;
- margin-bottom: 3ex;
- color: #666666;
- background: #FFFFFF;
-}
-
-#content p, #content ul, #content ol
-{
- font-size: 1em;
-}
-
-/* Blame IE6 and the guillotine bug for this one */
-#content { display: block; }
-
-#content h2
-{
- font-size: 1.6em;
- padding-top: .75em;
-}
-
-#content h3
-{
- color: #337acc;
- margin: 1.5ex 0 0.5ex;
- font-size: 1.4em;
-}
-
-#content h3.header {
- display: block;
-}
-
-#content table
-{
- border-collapse: collapse;
-}
-
-#content table th
-{
- background: #DDDDDD;
-}
-
-#content table th, #content table td
-{
- border: 1px solid #000000;
- padding: 0.6ex;
-}
-
-#content p
-{
- margin: 1ex 5ex 1ex 0;
-}
-
-#content ul, #content ol {
- margin: 0 3ex 2ex;
-}
-
-#content ul
-{
- list-style: square;
-}
-
-#content img
-{
- margin: 2ex 0;
-}
-
-
-
-#content .download
-{
- list-style-image: url(../images/arrow.png);
- list-style-position: outside;
- margin: 1.6ex 3ex;
-}
-
-#content .download li
-{
- color: #777777;
- font-size: 1.1em;
- background: #EFEFEF;
- padding: 1ex;
- border-bottom: 1px dotted #AAAAAA;
-}
-
-#content .download a
-{
- font-weight: bold;
-}
-
-#content .roles
-{
- margin: 0!important;
- list-style: none!important;
- display: inline;
-}
-
-#content .roles li
-{
- float: left;
- width: 130px;
- margin-bottom: 0.5ex;
-}
-
-#content .roles a
-{
- line-height: 1;
- height: 150px;
- text-align: center;
- display: block;
- text-decoration: none;
-}
-
-#content .roles a:hover
-{
- background-color: #EFEFEF;
-}
-
-#footer
-{
- position: relative;
- font: normal 0.8em/1.5 sans-serif;
- text-align: center;
- background: white url(../images/line-bottom.png) 0 0 repeat-x;
- margin: -140px 0 0;
- border-bottom: 10px solid #337ACC;
- height: 130px;
- color: #4D4D4D;
-}
-
-#bottom
-{
- margin-left: 18px;
- padding-right: 18px;
-}
-
-#footer ul
-{
- list-style: none;
- margin-bottom: .8em;
-}
-
-#footer li
-{
- display: inline;
- border-left: 1px solid #AAAAAA;
- padding: 0 0.75ex;
- margin-left: -1px;
-}
-
-#footer li.first
-{
- border-left: none;
-}
-
-#footer a
-{
- color: #3771C8;
-}
-
-#footer a:hover
-{
- color: #112233;
-}
-
-#footer p
-{
- margin: 0 1.5ex;
-}
-
-#footer .copy
-{
-}
-
-#footer .rh-logo {
- padding-top: 3ex;
- margin-bottom: 1em;
-}
-s
-#screenshot-banner {
- text-align: left;
-}
-
-input, textarea, select
-{
- border: #AAA 1px solid;
-}
-
-input, textarea, option
-{
- padding: 2px 5px 2px 5px;
-}
-
-#content #sponsors
-{
- list-style: none;
- margin: 0;
- padding: 0;
- overflow: auto;
-}
-
-#content #sponsors li
-{
- text-align: center;
- float: left;
- height: 100px;
- margin: 0 1ex;
-}
-
-#content #sponsors li img
-{
- display: block;
-}
-
-#content #primary-sponsors
-{
- margin: 0 0 4ex;
- padding: 0;
- overflow: auto;
- border-bottom: 1px #AAAAAA solid;
-}
-
-#content #primary-sponsors .psponsor
-{
- text-align: center;
- margin: 0 1ex;
-}
-
-#content #primary-sponsors .psponsor td
-{
- border-style: none;
-}
-
-#content #primary-sponsors .psponsor img
-{
- display: block;
- float: left;
- margin-right: 6ex;
-}
-
-#content #primary-sponsors .psponsor p
-{
- display: block;
- text-align: left;
-}
-
-#content ul.downloadbox
-{
- overflow: hidden;
- list-style: none;
- margin: 0 2ex;
- padding: 6ex 2ex 4ex 180px;
- border-radius: 1.5ex;
- -webkit-border-radius: 1.5ex;
- -moz-border-radius: 1.5ex;
- background: #C0DBDD 30px 50% no-repeat;
- font-size: 0.9em;
- display: inline-block;
-}
-
-#content ul.installdvd { background-image: url(../images/installdvd.png); }
-#content ul.gnomelive { background-image: url(../images/gnomelive.png); }
-#content ul.kdelive { background-image: url(../images/kdelive.png); }
-
-.downloadbox li
-{
- width: 32ex;
- float: left;
- padding: 0 0 2ex;
-}
-
-.downloadbox li li
-{
- width: auto;
- float: none;
- padding: 0;
-}
-
-
-#content pre.command
-{
- font-size: 1.3em;
- border: solid #DDDDDD;
- border-width: 1px 1px 1px 5px;
- background: #F2F5F6;
- padding: 0.5ex 2ex;
-}
-
-#content p.mini-note
-{
- padding: 1ex 2ex;
- border: 1px solid #DDDDDD;
- background: #FFABAB;
- font-size: .8em;
- color: #4c4c4c;
- -webkit-border-radius: 1.5ex;
- -moz-border-radius: 1.5ex;
- width: 80%
-}
-
-#content p.note
-{
- padding: 1ex 2ex;
- border: 1px solid #DDDDDD;
- background: #F2F5F6;
-}
-
-#content p.warning
-{
- padding: 1ex 2ex;
- border: 1px solid #DDDDDD;
- background: #FF6666;
- color: #000000;
-}
-
-#content p.warning a
-{
- color: #00154F;
-}
-#content ul#resources
-{
- list-style: none;
-}
-
-#content ul#resources a
-{
- padding-left: 20px;
- background: url(../images/arrow.png) 0 50% no-repeat;
-}
-
-#content ul#resources li
-{
- padding-left: 70px;
- margin-bottom: 3.5ex;
-}
-
-ul#resources li.resource-docs
-{
- background: url(../images/icon-docs.png) 0 50% no-repeat;
-}
-
-ul#resources li.resource-communicate
-{
- background: url(../images/icon-communicate.png) 0 50% no-repeat;
-}
-
-ul#resources li.resource-download
-{
- background: url(../images/icon-download.png) 0 50% no-repeat;
-}
-
-#content img.smirk
-{
- margin: 0 1ex 0 0;
-}
-
-#content p.joinlink
-{
- border: 1px solid #DDDDDD;
- background: #EEEEEE;
- padding: 1ex 5ex 1ex 5ex;
- text-align: center;
- font-size: 120%;
- font-weight: bold;
-}
-
-#content #subprojects
-{
- margin-right: 5ex;
- padding-top: 1ex;
-}
-
-#content a:link
-{
- color: #337ACC;
- text-decoration: none;
-}
-
-#content a:visited
-{
- color: #729FCF;
- text-decoration: none;
-}
-
-/* get-fedora styles */
-
-.download-block .button:hover {
- background-color: #9ECFFF !important;
-}
-
-.download-block {
- margin-right: 23em;
-}
-
-.download-sidebar {
- float: right;
- width: 21em;
- margin-left: 2em;
-}
-
-.download-sidebar a {
- font-size: 1.5em;
- display: block;
- padding: 1em;
- margin: 1em 0;
-}
-
-#other {
- margin-top: 2ex;
- background-color: #F0F0F0!important;
-}
-
-.download-block h3 {
- margin-top: 0!important;
-}
-
-
-.download-block em.byline {
- display: block;
- color: #999999;
- font-size: 0.8em;
-}
-
-.download-block em.download-tagline {
- color: #999999;
- font-size: 0.8em;
-}
-
-.download-block .button {
- border-radius: 5px;
- -webkit-border-radius: 5px;
- -moz-border-radius: 5px;
- color: #FFFFFF!important;
- text-decoration: none;
- font-weight: bold;
- line-height: 3;
- margin: 0;
- padding: 0.4em 0.5em;
- border: 1px solid #446382;
- background-color: #5F94C9 !important;
- margin-top: 0.9em;
-}
-
-.big-button {
- border-radius: 5px;
- -webkit-border-radius: 5px;
- -moz-border-radius: 5px;
- color: #FFFFFF!important;
- text-decoration: none;
- font-weight: bold;
- line-height: 3;
- margin: 0;
- padding: 0.4em 0.5em;
- background-color: #337acc !important;
- margin-top: 0.9em;
- font-size: 1.7em;
-}
-
-.big-button:hover {
- background-color: #295596 !important;
-}
-
-
-.fine-print {
- font-size: .9em !important;
- color: #999999;
- font-style: italic;
-}
-
-.resources-pane {
- float: right;
- background-color: #E5E5E5;
- padding: 0.5em 1em;
- border-radius: 5px;
- -webkit-border-radius: 5px;
- -moz-border-radius: 5px;
- margin-left: 1em;
-}
-
-.download-block p img {
- float: left;
- margin-right: 1.4em !important;
-}
-
-/** sidebar boxes for get-fedora **/
-
-.download-sidebar a {
- display: block;
- background: #337ACC url(../images/gradient_bg.png) repeat-x;
- border-radius: 8px;
- -webkit-border-radius: 8px;
- -moz-border-radius: 8px;
-}
-
-.download-sidebar a.even {
- background-color: #1855A2;
-}
-
-.download-sidebar a:hover {
- background-color: #1158AA;
-}
-
-.download-sidebar a.even:hover {
- background-color: #003380;
-}
-
-.download-sidebar a {
- color: #FFFFFF!important;
- font-size: medium;
- text-decoration: none;
- font-weight: bold;
- line-height: 1.5;
-}
-
-.download-sidebar span#thereismorefedora a {
- display: block;
- background: none;
- margin: 0px;
- padding: 0px;
-}
-
-.download-sidebar span#thereismorefedora img {
- margin: 0px;
- padding: 0px;
-}
-
-#breadcrumbs {
- border-bottom: 1px solid #DDDDDD;
- margin-bottom: 1ex;
- margin-right: 24%;
-}
-
-#breadcrumbs a {
- text-decoration: none;
- font-size: 1em;
- color: #7788DD;
-}
-
-/** Web App Styles **/
-/* Links to other web apps */
-a.bodhi {
- padding-left: 20px;
- background: url(../images/icons/fedora-infra-icon_bodhi.png) 0 50% no-repeat;
-}
-
-a.koji {
- padding-left: 20px;
- background: url(../images/icons/fedora-infra-icon_koji.png) 0 50% no-repeat;
-}
-
-a.bugz {
- padding-left: 20px;
- background: url(../images/icons/fedora-infra-icon_bugzilla.png) 0 50% no-repeat;
-}
-
-a.pkgdb {
- padding-left: 20px;
- background: url(../images/icons/fedora-infra-icon_pkgdb.png) 0 50% no-repeat;
-}
-
-a.cvs {
- padding-left: 20px;
- background: url(../images/icons/fedora-infra-icon_source-control.png) 0 50% no-repeat;
-}
-
-/* tooltip */
-.tooltip {
- display: block;
- position: absolute;
- border: 1px solid black;
- background-color: #ffffdd;
- width: 200px;
-}
-
-/* style to hide an element */
-.invisible {
- display: none;
-}
-
-/* styles for tg flash messages */
-.flash {
- background: #DEE681;
- border: 1px solid #CCBBAA;
- padding: 1.5ex 15px 1.5ex 43px;
- margin: 1ex 0;
-}
-
-.flash .success {
- background: #DEE681 url(../images/success.png) 10px 50% no-repeat;
-}
-
-.flash .warning {
- background: #DEE681 url(../images/important.png) 10px 50% no-repeat;
-}
-
-.flash .info {
- background: #DEE681 url(../images/info.png) 10px 50% no-repeat;
-}
-
-/* toolbar styles */
-.toolbar {
- list-style: none;
- list-style-image: none;
- list-style-position: outside;
- list-style-type: none;
- text-align: right;
- color: #888888;
- font-size: 0.9em;
- text-transform: uppercase;
- vertical-align: middle;
-}
-
-.toolbar * {
- display: inline;
-}
-
-#head #main-toolbar {
- float: right;
- margin-top: 9px;
- width: 50%;
-}
-
-.toolbar input.button {
- background-color: #888888;
- color: white;
- font-size: 0.9em;
- margin-left: 5px;
- padding: 2px;
- text-transform: uppercase;
- vertical-align: middle;
-}
-
-/* panel styles */
-#content .panel {
- border: medium none;
- margin-top: 12px;
-}
-
-#content .panel h3 {
- -moz-border-radius-bottomleft: 0;
- -moz-border-radius-bottomright: 0;
- -moz-border-radius-topleft: 12px;
- -moz-border-radius-topright: 12px;
- background-color: #D2D2D2;
- color: white;
- font-size: 1em;
- padding: 8px 6px 6px;
- text-transform: uppercase;
- margin: 0;
-}
-
-/* Login panel */
-#login-box form .field {
- margin: 0 0 1ex;
- overflow: hidden;
- text-align: left;
-}
-
-#login-box .field label {
- clear: left;
- float:left;
- padding: 0 2ex 0 0;
- text-align: left;
- width: 32ex;
-}
-
-#content .login {
- -moz-border-radius-bottomleft: 10px;
- -moz-border-radius-bottomright: 10px;
- -moz-border-radius-topleft: 10px;
- -moz-border-radius-topright: 10px;
- background-color: #5F94C9;
- color: white;
- padding: 8px 12px;
-}
-
-#content div.login * {
- color: white;
- padding: 0;
- margin: 0;
-}
-
-#content .login h3 {
- background-color: #5F94C9;
-}
-
-#content .login input {
- padding: 2px 5px;
- margin-bottom: 5px;
- color: black;
-}
-
-#content .login label {
- color: white;
- display: block;
- float: left;
- font-size: 0.8em;
- margin-right: 10px;
- width: 60px;
- text-align: left;
-}
-
-#content .login .button {
- background-color: #777777;
- border: 1px solid #555555;
- color: white;
-}
-
-#content .login ul {
- list-style-position: inside;
-}
-
-#export-regulations {
- padding-top: 1em;
- margin-left: 2em;
-}
-
-#export-regulations .read-more img, #export-regulations .details img {
- margin: 0;
-}
-
-#export-regulations .details {
- display: inline;
-}
-
-#export-regulations a {
- text-decoration: none;
-}
-
-.read-more, .re-collapse {
- float:right;
-}
-
-.read-more img {
- float: none;
-}
-
-/* applies to get-* pages */
-
-#download-resources {
- float:right;
- margin-left:.5em;
- width:21em; /* tofix */
-}
-
-#download-resources h4 {
- text-transform: uppercase;
- margin-bottom: .5em;
-}
-
-#download-resources li {
- padding: 3px 0;
-}
-
-#download-resources ul.checkmark-list {
- list-style: disc url("/static/images/icon-checkmark.png") outside;
-}
-
-#download-resources li.guide-list {
- list-style: disc url("/static/images/icon-guide.png") outside;
-}
-
-#download-resources li.webpage-list {
- list-style: disc url("/static/images/icon-webpage.png") outside;
-}
-
-#download-resources li.support-list {
- list-style: disc url("/static/images/icon-support.png") outside;
-}
-
-#download-resources li.verify-list {
- list-style: disc url("/static/images/icon-signed-verified.png") outside;
-}
-
-#download-resources div, #download-resources div h4 {
- padding: 5px 0 5px 0;
-}
-
-#download-resources div p {
- padding-left: 5px;
-}
-
-#download-resources p,
-#download-resources ul {
- margin-bottom: 2em;
-}
-
-#download-resources ul {
- margin-left: 2em;
-}
-
-#download-options-area {
- background-color: #f2f2f2;
- padding: 15px;
-}
-
-#download-options-area h4 {
- color: #337ACC;
-}
-
-.other-options {
- float: right;
- border-radius: 5px;
- -webkit-border-radius: 5px;
- -moz-border-radius: 5px;
- margin-left: 1em;
- width: 45%;
-}
-
-.upgrade-section {
- width: 50%;
-}
-
-/* get-* format tab */
-.format-arch {
- border-bottom: 1px #E5E5E5 solid;
- float:left;
- width: 90%;
- padding: 5px;
-}
-
-.support-icon {
- border: 0px;
- padding: 3px;
-}
-
-.vendor-header {
- clear: left;
- font-size: 1.3em;
- padding-top: 15px;
- padding-bottom: 15px;
-}
-
-.freemedia-header {
- clear: left;
- font-size: 1.3em;
- padding-top: 15px;
-}
-
-
-.vendor1, .vendor2 {
- width: 50%;
-}
-
-/* end: applies to get-* pages */
-
-/* tabs */
-/* root element for tabs */
-ul.tabs {
- margin: 0 !important;
- padding: 0;
- height: 30px;
- border-bottom: 1px solid #dcdcdc;
- width: 70%;
-}
-
-/* single tab */
-ul.tabs li {
- float: left;
- padding: 0;
- margin: 0;
- list-style-type: none;
-}
-
-/* link inside the tab. no background image */
-ul.tabs a {
- float: left;
- font-size: 13px;
- display: block;
- padding: 5px 10px;
- text-decoration: none;
- border: 1px solid #dcdcdc;
- border-bottom: 0px;
- height: 18px;
- background-color: #dcdcdc;
- color: #3771c8;
- margin-right: 4px;
- -moz-border-radius-topleft: 4px;
- -moz-border-radius-topright: 4px;
- position: relative;
- top: 1px;
- text-transform: uppercase;
- font-weight: bold;
-}
-
-ul.tabs a:hover {
- background-color: #fff;
- color: #818181;
- text-transform: uppercase;
- font-weight: bold;
-}
-
-/* selected tab */
-ul.tabs a.current {
- background-color: #fff;
- border-bottom: 2px solid #fff;
- color: #808080;
- cursor: default;
- text-transform: uppercase;
- font-weight: bold;
-}
-
-/* tab pane */
-div.panes div {
- display: none;
- border: 0px;
- border-left: 1px solid #dcdcdc;
- min-height: 150px;
- padding: 15px 10px;
- /* tofix width: 70%; */
- float: left;
-}
-
-#content a.current:link {
- color: #828282;
-}
-/* end: tabs */
-
-/* --- */
-#content span.download-opt1
-{
- float: left;
- width: 45%;
-}
-
-div.panes span.download-option {
- display: block;
- /* tofix: width: 300px !important; */
- width: 50%;
-}
-
-div.panes span.opt1 { float: left; }
-div.panes span.opt2 {
- float: right;
- width: 50%;
-}
-
-#export-regulations {
- clear: both;
- margin-bottom: 2em;
-}
-
-/* Hack to make it simple to get rid of borders in a table (ianweller) */
-#content table.nobordersplz th, #content table.nobordersplz td { border-width: 0px; }
-
-/* get-option common */
-/* keeps common stuff in get pages */
-.star-fedora-default, .star-most-compatible {
- background-color: #F7FF9D;
- border: 1px solid #A19800;
- padding: 0.2em;
- font-weight: bold;
- color: #A19800;
- font-size: 0.8em;
- margin: 7px;
- text-transform: uppercase;
- white-space: nowrap;
-}
-
-.download-button-mini {
- border-radius: 5px;
- -webkit-border-radius: 5px;
- -moz-border-radius: 5px;
- color: #FFFFFF!important;
- text-decoration: none;
- font-weight: bold;
- line-height: 2;
- font-size:0.85em;
- padding: 5px;
- background-color: #337ACC !important;
- white-space: nowrap;
-}
-
-.download-button-mini:hover {
- background-color: #295596 !important;
-}
-
-.opt1 { float: left; }
-.opt2 { float: right; }
-
-#content .format-arch .opt1 p, #content .format-arch .opt1 h3, #content .format-arch .opt2 p, #content .format-arch .opt2 h3 {
- padding: 0px;
- margin: 0px
-}
-
-.panes {
- width: 68%
-}
-
-.shownojs { display: none; }
-
-.area-callout
-{
- display: inline-block; /* to put icons next to header */
-}
-
-/* end: get-option common */
-
-/* get-option - desktop pane */
-#content .desktop-section table, #content .desktop-section table td {
- border: 0px;
-}
-
-.desktop-section ul {
- float: left;
-}
-
-.desktop-section img.desktop-screenshot {
- float: right;
-}
-
-.desktop-section table tbody tr {
- background-color: #fff;
-}
-
-.desktop-section td.description {
- max-width: 230px;
-}
-
-.desktop-section td {
- vertical-align: top;
-}
-
-img.desktop-screenshot:hover {
- cursor: pointer;
- cursor: hand;
-}
-
-/* end: get-option - desktop pane */
-
-
-/* get-option - architecture pane */
-.arch-section, .desktop-section {
- clear:left;
- display:block;
- margin-bottom:1em;
- padding:0.5em 1em;
-}
-
-img.download-media
-{
- float: left;
- padding-right: 7px;
-}
-
-.arch-sparc-area {
- background-color: #f5f5f5;
- padding: 1em;
- clear: both;
- display: block;
-}
-
-.arch-section h3 span {
- color: black;
-}
-
-/* end: get-option - architecture pane */
-
-.panes table tr {
- background-color:#F2F2F2;
- border:0 none;
-}
-
-#get-acitivites td.rank {
- background-color:white;
- font-weight:bold;
- padding-left:0;
- padding-right:7px;
- text-align:left;
-}
-
-#get-acitivites td.name {
- font-size:xx-small;
- font-style:italic;
-}
-
-#get-acitivites table td {
- border-color: white !important;
- border-style: solid !important;
- border-width: 0 0 18px !important;
- padding: 0.8em;
- vertical-align: top;
-}
-
-.learn-more {
- display: none;
-}
-
-/*
- * note: probably not needed
-#content table th, #content table td {
- border:1px solid #000000;
- padding:0.6ex;
-}
-*/
-
-/* end: get-option - activites pane */
-
-#content img {
- margin:0;
-}
-
-
-
-/* make it work in 1024x786 */
-
-#desktop-versions {
- width: 100%;
-}
-
-.panes {
- min-width: 50%;
-}
-
-.panes table {
- width: 80%;
-}
-
-#download-resources {
- min-width: 200px;
- width: 20%;
-}
-
-
-#content .panes {
- min-width: 400px;
-}
-
-#nav {
- margin: 0 1em;
- padding:0 0 0 1.5em;
-}
-
-tr.dl-buttons td {
- min-width: 104px !important;
-}
-
-div.panes div {
- background-color: transparent !important;
- padding-right: 15 5 15 10;
-}
-
-.arch-section,
-.desktop-section {
- padding-right: 0.5em 0 0.5em 1em;
-}
-
-/* the overlayed element */
-.simple_overlay {
-
- /* must be initially hidden */
- display:none;
-
- /* place overlay on top of other elements */
- z-index:10000;
-
- /* styling */
- background-color:#333;
-
- /* disabled for now - so it fits around images
- * width:675px;
- */
- min-height:200px;
- border:1px solid #666;
-
- /* CSS3 styling for latest browsers */
- -moz-box-shadow:0 0 90px 5px #000;
- -webkit-box-shadow: 0 0 90px #000;
-}
-
-/* close button positioned on upper right corner */
-.simple_overlay .close {
- background-image:url(../images/overlay-close.png);
- position:absolute;
- right:-15px;
- top:-15px;
- cursor:pointer;
- height:35px;
- width:35px;
-}
-
-/* styling for elements inside overlay */
-.overlay_details {
- position:absolute;
- top:15px;
- right:15px;
- font-size:11px;
- color:#fff;
- width:150px;
-}
-
-.overlay_details h3 {
- color:#aba;
- font-size:15px;
- margin:0 0 -10px 0;
-}
-
-/* end: the overlayed element */
-
-.global-notice {
- background-color: #f6f6f6;
- border: 1px solid #ddd;
- padding: 0.5em 1em;
-}
diff --git a/httpd-fedora-elections.conf.in b/httpd-fedora-elections.conf.in
index f681506..c974b88 100644
--- a/httpd-fedora-elections.conf.in
+++ b/httpd-fedora-elections.conf.in
@@ -1,4 +1,5 @@
-Alias /fedora-elections/static @DATADIR@/fedora_elections/static
+Alias /fedora-elections/images @DATADIR@/fedora-elections/images
+Alias /fedora-elections/static @DATADIR@/fedora-elections/static
WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
WSGISocketPrefix run/wsgi
@@ -6,7 +7,7 @@ WSGIRestrictStdout On
WSGIRestrictSignal Off
WSGIPythonOptimize 1
-WSGIScriptAlias /fedora-elections @SBINDIR(a)/fedora-elections.wsgi
+WSGIScriptAlias /fedora-elections @DATADIR(a)/fedora-elections/fedora-elections.wsgi
<Location /fedora-elections>
WSGIProcessGroup fedora-elections
commit b4fd5269695344e0c634b78a16df2dc93be8f090
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Thu May 9 03:52:52 2013 +0000
Change for RPM
diff --git a/fedora-elections.cfg.sample b/fedora-elections.cfg.sample
new file mode 100644
index 0000000..1dff33a
--- /dev/null
+++ b/fedora-elections.cfg.sample
@@ -0,0 +1,18 @@
+import os, os.path
+
+DEBUG = True
+
+
+# You will want to change this for your install
+SECRET_KEY = 'change me'
+SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.getcwd(), 'database.sqlite')
+
+FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
+FAS_USERNAME = 'elections'
+FAS_PASSWORD ='fake_fas_password'
+FAS_CHECK_CERT = False
+FAS_HTTPS_REQUIRED = False
+
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+# List of URLs for additional site-wide CSS
+EXTRA_CSS = []
commit 07d831bc8b7c5bae9e56d6b1e2950b99b90df473
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Thu May 9 03:52:20 2013 +0000
Change for RPM
diff --git a/fedora-elections.spec b/fedora-elections.spec
index c24cb39..7eff44d 100644
--- a/fedora-elections.spec
+++ b/fedora-elections.spec
@@ -6,35 +6,58 @@ Summary: Fedora elections application
Group: Development/Languages
License: GPLv2+
URL: https://github.com/fedora-infra/elections
-Source0: https://fedorahosted.org/releases/f/a/%{name}/%{name}-%{version}.tar.gz
-
+Source0: %{name}/%{name}-%{version}.tar.gz
+# Source0: https://fedorahosted.org/releases/f/a/%{name}/%{name}-%{version}.tar.gz
BuildArch: noarch
+BuildRequires: libxslt
BuildRequires: python
-BuildRequires: python-devel
-BuildRequires: python-beaker
+BuildRequires: python-babel
+BuildRequires: python-backports-ssl_match_hostname
+BuildRequires: python-bunch
+BuildRequires: python-chardet
BuildRequires: python-fedora
BuildRequires: python-fedora-flask
BuildRequires: python-flask
-BuildRequires: python-flask-babel
BuildRequires: python-flask-sqlalchemy
-BuildRequires: python-flask-wtforms
+BuildRequires: python-flask-wtf
+BuildRequires: python-jinja2
BuildRequires: python-kitchen
+BuildRequires: python-lxml
+BuildRequires: python-openid
+BuildRequires: python-ordereddict
+BuildRequires: python-requests
BuildRequires: python-setuptools
-BuildRequires: python-setuptools-devel
+BuildRequires: python-simplejson
+BuildRequires: python-six
BuildRequires: python-sqlalchemy0.7
-
-Requires: python-beaker
+BuildRequires: python-urllib3
+BuildRequires: python-wtforms
+
+Requires: libxslt
+Requires: python
+Requires: python-babel
+Requires: python-backports-ssl_match_hostname
+Requires: python-bunch
+Requires: python-chardet
Requires: python-fedora
Requires: python-fedora-flask
Requires: python-flask
-Requires: python-flask-babel
Requires: python-flask-sqlalchemy
-Requires: python-flask-wtforms
+Requires: python-flask-wtf
Requires: python-jinja2
Requires: python-kitchen
+Requires: python-lxml
+Requires: python-openid
+Requires: python-ordereddict
+Requires: python-requests
+Requires: python-setuptools
+Requires: python-simplejson
+Requires: python-six
Requires: python-sqlalchemy0.7
+Requires: python-urllib3
+Requires: python-wtforms
%description
@@ -42,6 +65,7 @@ fedora-elections is the Fedora Elections application.
%prep
%setup -q
+./configure.py
%build
@@ -58,7 +82,8 @@ rm -rf $RPM_BUILD_ROOT
%{__mkdir_p} %{buildroot}%{_datadir}/%{name}
%{__mkdir_p} %{buildroot}%{_datadir}/%{name}/static
-%{__install} -m 644 fedora_elections.conf %{buildroot}%{_sysconfdir}/httpd/conf.d/fedora_elections.conf
+%{__install} -m 644 httpd-fedora-elections.conf \
+ %{buildroot}%{_sysconfdir}/httpd/conf.d/httpd-fedora-elections.conf
%{__install} -m 644 fedora_elections/static/* %{buildroot}%{_datadir}/%{name}/static
%{__install} -m 644 %{name}.cfg.sample %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
%{__install} -m 644 createdb.py %{buildroot}%{_datadir}/%{name}/createdb.py
@@ -75,11 +100,11 @@ rm -rf $RPM_BUILD_ROOT
%doc
%dir %{_sysconfdir}/%{name}
%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
-%config(noreplace) %{_sysconfdir}/httpd/conf.d/%{name}.conf
+%config(noreplace) %{_sysconfdir}/httpd/conf.d/httpd-%{name}.conf
%{_datadir}/%{name}
%{python_sitelib}/*
%changelog
-* Wed Mar 06 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1
+* Sat May 04 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1
- Creation
diff --git a/setup.py b/setup.py
index e5ffd25..83a6354 100644
--- a/setup.py
+++ b/setup.py
@@ -12,5 +12,5 @@ setup( name = 'fedora-elections'
, packages = find_packages()
, zip_safe = False
, include_package_data = True
- , install_requires = ['Flask', 'SQLAlchemy>=0.7', 'flask-sqlalchemy', 'beaker']
+ , install_requires = ['Flask', 'SQLAlchemy>=0.7', 'flask-sqlalchemy']
)
commit 3cb86d139e47811f05d0a5a450afebde7d0631c4
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Tue Apr 30 04:02:50 2013 +0000
httpd/wsgi now working
diff --git a/.gitignore b/.gitignore
index bb585f9..7ecf5f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,7 @@ database.sqlite
*.pyc
*.pyo
*.swp
+development.cfg
+fedora-elections.cfg
+fedora-elections.wsgi
+httpd-fedora-elections.conf
diff --git a/README b/README
new file mode 100644
index 0000000..69d11ea
--- /dev/null
+++ b/README
@@ -0,0 +1,95 @@
+================
+Fedora Elections
+===============
+
+* What is fedora-election?
+fedora-elections is a web application written in python and based on flask.
+It implements the Range Voting system (http://rangevoting.org).
+
+This project was developed using fedoraproject requests, but, can be easily
+adapted to other projects. Fedora-elections is integrated with the Fedora
+Account System (FAS).
+
+
+Running from a checkout:
+=======================
+
+Install Prerequisites
+---------------------
+Before launching fedora-elections, the following packages should be installed:
+ - httpd
+ - libxslt
+ - python
+ - python-backports-ssl_match_hostname
+ - python-bunch
+ - python-chardet
+ - python-fedora
+ - python-fedora-flask
+ - python-flask
+ - python-flask-sqlalchemy
+ - python-flask-wtf
+ - python-jinja2
+ - python-kitchen
+ - python-lxml
+ - python-openid
+ - python-ordereddict
+ - python-ordereddict
+ - python-setuptools
+ - python-simplejson
+ - python-six
+ - python-sqlalchemy0.7
+ - python-urllib3
+ - python-wtforms
+
+
+Get the source code
+-------------------
+You can obtain the code from github:
+ https://github.com/fedora-infra/elections.git
+
+
+Configure the application
+-------------------------
+./configure.py --install-conf=`pwd` --install-data=`pwd` --install-sbin=`pwd`
+
+This will create fedora-elections.wsgi and http-fedora-elections.conf.
+
+
+Create a database
+-----------------
+Copy development.cfg.sample to development.cfg.
+Review the file and make changes as appropriate.
+Run:
+ ./createdb
+
+
+Starting the Application
+------------------------
+
+There are 2 ways to start the application:
+ o without http
+ o with http
+
+
+* How to start without http on localhost:5000:
+ cd elections
+ ./runserver
+
+
+* How to start with http
+ Next copy the httpd-pkgdb.conf file to your apache conf.d directory:
+
+ sudo cp httpd-fedora-elections.conf /etc/httpd/conf.d/.
+
+ Restart apache:
+
+ sudo /etc/init.d/httpd restart
+
+* How to contribute
+If you find bug or want to propose ideas or stuff to be implemented or if
+you are interested to became a developer for this project just
+ask on #fedora-admin irc channel on irc.freenode.net or use our
+web site https://fedorahosted.org/elections.
+
+* Licence
+fedora-elections is licenced under GPL v2.
diff --git a/configure.py b/configure.py
new file mode 100755
index 0000000..9ea3f51
--- /dev/null
+++ b/configure.py
@@ -0,0 +1,77 @@
+#!/usr/bin/python -tt
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2013 Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+#
+# Author(s): Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+
+import glob
+import os
+import re
+
+from optparse import OptionParser
+
+#
+# Process any command line options/switches.
+#
+parser = OptionParser()
+parser.add_option('--install-conf', dest='confdir', action='store',
+ default='/etc',
+ help='Where to install configuration files.')
+parser.add_option('--install-data', dest='datadir', action='store',
+ default='/usr/share',
+ help='Where to install data files.')
+parser.add_option('--install-sbindir', dest='sbindir', action='store',
+ default='/usr/sbin',
+ help='Where to install executeable files.')
+
+(options, args) = parser.parse_args()
+
+confdir = options.confdir
+datadir = options.datadir
+sbindir = options.sbindir
+
+print " confdir = %s" % confdir
+print " datadir = %s" % datadir
+print " sbindir = %s" % sbindir
+
+substs = {'@CONFDIR@':confdir,
+ '@DATADIR@':datadir,
+ '@SBINDIR@':sbindir
+ }
+
+pattern = re.compile("\.in$")
+
+files = glob.glob('./*.in')
+
+#
+# Process all *.in files in the current directory.
+#
+if (files is not []):
+ for orig_fn in files:
+ if os.path.isfile(orig_fn):
+ new_fn = re.sub(pattern, '', orig_fn)
+ print " Creating %s from %s" % (new_fn, orig_fn)
+ fp1 = open(orig_fn, "r")
+ file_contents = fp1.read()
+ fp1.close()
+
+ fp2 = open(new_fn, "w")
+ for key, value in substs.iteritems():
+ file_contents = file_contents.replace(key, value)
+ fp2.write(file_contents)
+ fp2.close()
diff --git a/development.cfg.sample b/development.cfg.sample
new file mode 100644
index 0000000..1dff33a
--- /dev/null
+++ b/development.cfg.sample
@@ -0,0 +1,18 @@
+import os, os.path
+
+DEBUG = True
+
+
+# You will want to change this for your install
+SECRET_KEY = 'change me'
+SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.getcwd(), 'database.sqlite')
+
+FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
+FAS_USERNAME = 'elections'
+FAS_PASSWORD ='fake_fas_password'
+FAS_CHECK_CERT = False
+FAS_HTTPS_REQUIRED = False
+
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+# List of URLs for additional site-wide CSS
+EXTRA_CSS = []
diff --git a/fedora-elections.cfg.in b/fedora-elections.cfg.in
new file mode 100644
index 0000000..fc0d50d
--- /dev/null
+++ b/fedora-elections.cfg.in
@@ -0,0 +1,18 @@
+import os, os.path
+
+DEBUG = True
+
+
+# You will want to change this for your install
+SECRET_KEY = 'change me'
+SQLALCHEMY_DATABASE_URI = 'sqlite:///@DATADIR@/database.sqlite'
+
+FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
+FAS_USERNAME = 'elections'
+FAS_PASSWORD ='el3ctMEn0w'
+FAS_CHECK_CERT = False
+FAS_HTTPS_REQUIRED = False
+
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+# List of URLs for additional site-wide CSS
+EXTRA_CSS = []
diff --git a/fedora-elections.spec b/fedora-elections.spec
new file mode 100644
index 0000000..c24cb39
--- /dev/null
+++ b/fedora-elections.spec
@@ -0,0 +1,85 @@
+Name: fedora-elections
+Version: 0.1
+Release: 1%{?dist}
+Summary: Fedora elections application
+
+Group: Development/Languages
+License: GPLv2+
+URL: https://github.com/fedora-infra/elections
+Source0: https://fedorahosted.org/releases/f/a/%{name}/%{name}-%{version}.tar.gz
+
+
+BuildArch: noarch
+
+BuildRequires: python
+BuildRequires: python-devel
+BuildRequires: python-beaker
+BuildRequires: python-fedora
+BuildRequires: python-fedora-flask
+BuildRequires: python-flask
+BuildRequires: python-flask-babel
+BuildRequires: python-flask-sqlalchemy
+BuildRequires: python-flask-wtforms
+BuildRequires: python-kitchen
+BuildRequires: python-setuptools
+BuildRequires: python-setuptools-devel
+BuildRequires: python-sqlalchemy0.7
+
+Requires: python-beaker
+Requires: python-fedora
+Requires: python-fedora-flask
+Requires: python-flask
+Requires: python-flask-babel
+Requires: python-flask-sqlalchemy
+Requires: python-flask-wtforms
+Requires: python-jinja2
+Requires: python-kitchen
+Requires: python-sqlalchemy0.7
+
+
+%description
+fedora-elections is the Fedora Elections application.
+
+%prep
+%setup -q
+
+
+%build
+# Remove CFLAGS=... for noarch packages (unneeded)
+CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
+
+
+%install
+rm -rf $RPM_BUILD_ROOT
+%{__python} setup.py install -O1 --skip-build --install-data=%{_datadir} --root $RPM_BUILD_ROOT
+
+%{__mkdir_p} %{buildroot}%{_sysconfdir}/%{name}
+%{__mkdir_p} %{buildroot}%{_sysconfdir}/httpd/conf.d
+%{__mkdir_p} %{buildroot}%{_datadir}/%{name}
+%{__mkdir_p} %{buildroot}%{_datadir}/%{name}/static
+
+%{__install} -m 644 fedora_elections.conf %{buildroot}%{_sysconfdir}/httpd/conf.d/fedora_elections.conf
+%{__install} -m 644 fedora_elections/static/* %{buildroot}%{_datadir}/%{name}/static
+%{__install} -m 644 %{name}.cfg.sample %{buildroot}%{_sysconfdir}/%{name}/%{name}.cfg
+%{__install} -m 644 createdb.py %{buildroot}%{_datadir}/%{name}/createdb.py
+%{__install} -m 644 %{name}.wsgi %{buildroot}%{_datadir}/%{name}/%{name}.wsgi
+
+
+
+%clean
+rm -rf $RPM_BUILD_ROOT
+
+
+%files
+%defattr(-,root,root,-)
+%doc
+%dir %{_sysconfdir}/%{name}
+%config(noreplace) %{_sysconfdir}/%{name}/%{name}.cfg
+%config(noreplace) %{_sysconfdir}/httpd/conf.d/%{name}.conf
+%{_datadir}/%{name}
+%{python_sitelib}/*
+
+
+%changelog
+* Wed Mar 06 2013 Frank Chiulli <fchiulli(a)fedoraproject.org> - 0.1
+- Creation
diff --git a/fedora-elections.wsgi.in b/fedora-elections.wsgi.in
new file mode 100644
index 0000000..c7cfbb6
--- /dev/null
+++ b/fedora-elections.wsgi.in
@@ -0,0 +1,22 @@
+#-*- coding: UTF-8 -*-
+
+# These two lines are needed to run on EL6
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+import os
+import sys
+
+sys.stdout = sys.stderr
+
+## Set the environment variable pointing to the configuration file
+os.environ['FEDORA_ELECTIONS_CONFIG'] = '@CONFDIR(a)/fedora-elections.cfg'
+
+## The following is only needed if you did not install
+## as a python module (for example if you run it from a git clone).
+import sys
+# sys.path.insert(0, '@SBINDIR@/fedora_elections/')
+sys.path.insert(0, '@SBINDIR@')
+
+from fedora_elections import app as application
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index e0b4ef5..18cf36f 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -96,7 +96,9 @@ def election_admin_required(f):
@app.route('/')
def index():
elections = models.Election.query.filter_by(frontpage=True).all()
+ num_elections = len(elections)
return flask.render_template('list/index.html', elections=elections,
+ num_elections=num_elections,
title="Elections")
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 2985fcc..290c2cf 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -1,5 +1,6 @@
{% extends "base.html" %}
{% block body %}
+ {% if num_elections > 0 %}
<h2>{{ title }}</h2>
<table>
<tr>
@@ -27,4 +28,5 @@
</tr>
{% endfor %}
</table>
+ {% endif %}
{% endblock %}
diff --git a/httpd-fedora-elections.conf.in b/httpd-fedora-elections.conf.in
new file mode 100644
index 0000000..f681506
--- /dev/null
+++ b/httpd-fedora-elections.conf.in
@@ -0,0 +1,22 @@
+Alias /fedora-elections/static @DATADIR@/fedora_elections/static
+
+WSGIDaemonProcess fedora-elections user=apache group=apache maximum-requests=1000 display-name=fedora-elections processes=2 threads=1
+WSGISocketPrefix run/wsgi
+WSGIRestrictStdout On
+WSGIRestrictSignal Off
+WSGIPythonOptimize 1
+
+WSGIScriptAlias /fedora-elections @SBINDIR(a)/fedora-elections.wsgi
+
+<Location /fedora-elections>
+ WSGIProcessGroup fedora-elections
+ <IfModule mod_authz_core.c>
+ # Apache 2.4
+ Require all granted
+ </IfModule>
+ <IfModule !mod_authz_core.c>
+ # Apache 2.2
+ Order deny,allow
+ Allow from all
+ </IfModule>
+</Location>
commit 0316694d783556284b54b6aa70bb2c80cdaa05b4
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Wed Feb 27 23:29:37 2013 -0500
Correct version
diff --git a/setup.py b/setup.py
index 47357d3..e5ffd25 100644
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ import pkg_resources
from setuptools import setup, find_packages
setup( name = 'fedora-elections'
- , version = '@VERSION@'
+ , version = '0.1'
, author = 'Frank Chiulli'
, author_email = 'fchiulli(a)fedoraproject.org'
, packages = find_packages()
commit 41cb17c7ee7ab04e1d3e77cbb79081ba9425efeb
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Wed Feb 27 22:35:59 2013 -0500
Update documentation
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..0f3bd00
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,4 @@
+Ralph Bean <rbean(a)redhat.com>
+Frank Chiulli <fchiulli(a)fedoraproject.org>
+Toshio Kuratomi <toshio(a)fedoraproject.org>
+Ian Weller <ianweller(a)fedoraproject.org>
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..7a12f1b
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,343 @@
+ GNU GENERAL PUBLIC LICENSE
+Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software
+ interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Program does not specify a
+version number of this License, you may choose any version ever
+published by the Free Software Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+END OF TERMS AND CONDITIONS
+
+How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these
+terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..47357d3
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,16 @@
+## These two lines are needed to run on EL6
+__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
+import pkg_resources
+
+
+from setuptools import setup, find_packages
+
+setup( name = 'fedora-elections'
+ , version = '@VERSION@'
+ , author = 'Frank Chiulli'
+ , author_email = 'fchiulli(a)fedoraproject.org'
+ , packages = find_packages()
+ , zip_safe = False
+ , include_package_data = True
+ , install_requires = ['Flask', 'SQLAlchemy>=0.7', 'flask-sqlalchemy', 'beaker']
+ )
commit f324f1c5ea5482fcf640c35d288593ed5b8a817b
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Wed Feb 27 22:35:32 2013 -0500
Fix AccountSystem call
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index d2ded05..e0b4ef5 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -56,7 +56,8 @@ fas = FAS(app)
fas2_url = app.config['FAS_BASE_URL']
fas2_username = app.config['FAS_USERNAME']
fas2_password = app.config['FAS_PASSWORD']
-fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password)
+fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password,
+ insecure=True)
# modular imports
commit e65990eaa653054e4be9848c12aad5dbb593ad23
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Sun Feb 17 23:01:56 2013 -0500
Test again
commit 1b8e2ecf739de797401e9539cb94c02855790e55
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Sun Feb 17 22:49:05 2013 -0500
Test git push
commit c961c2cde24f34c8d0c930ab696ca1559a3e1823
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Sun Feb 17 19:05:01 2013 -0500
Lots of updates/changes
diff --git a/TODO b/TODO
index 9858c8f..ebace48 100644
--- a/TODO
+++ b/TODO
@@ -2,8 +2,6 @@ Things that need doing to get this deployed
===========================================
* Add legalvoters to an election
-* Add page to vote
-* Add page that summarizes the vote results
* Write a conversion script for the old db model to the new one
* rpmify
* rpmify flask-fas (or get flask-fas into python-fedora)
@@ -19,7 +17,6 @@ Future enhancements (anything not necessary for deploying)
==========================================================
* Make voting methods modular
-* Give this a Fedora theme
* Break the main program logic out into library modules
** Controllers should be as lightweight as possible
*** validation of input
diff --git a/development.cfg b/development.cfg
index 6e045f2..e4b83bb 100644
--- a/development.cfg
+++ b/development.cfg
@@ -1,11 +1,15 @@
import os, os.path
DEBUG = True
+
+
# You will want to change this for your install
SECRET_KEY = 'change me'
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.getcwd(), 'database.sqlite')
FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
+FAS_USERNAME = 'elections'
+FAS_PASSWORD ='el3ctMEn0w'
FAS_CHECK_CERT = False
FAS_HTTPS_REQUIRED = False
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 78d2a16..d2ded05 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -1,7 +1,37 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2012 Red Hat, Inc.
+# Copyright © 2012 Ian Weller <ianweller(a)fedoraproject.org>
+# Copyright © 2012 Toshio Kuratomi <tkuratom(a)redhat.com>
+# Copyright © 2012 Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2, or (at your option) any later version. This
+# program is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the GNU
+# General Public License along with this program; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
+# code or documentation are not subject to the GNU General Public License and
+# may only be used or replicated with the express permission of Red Hat, Inc.
+#
+# Author(s): Ian Weller <ianweller(a)fedoraproject.org>
+# Toshio Kuratomi <tkuratom(a)redhat.com>
+# Frank Chiulli <fchiulli(a)fedoraproject.org>
+#
+
import flask
from flask.ext.fas import FAS
from flask.ext.sqlalchemy import SQLAlchemy
+from fedora.client import AuthError, AppError
+from fedora.client.fas2 import AccountSystem
+
+from sqlalchemy.orm.exc import NoResultFound
+
import fedmsgshim
from datetime import datetime, time
@@ -18,6 +48,16 @@ db = SQLAlchemy(app)
# set up FAS
fas = FAS(app)
+# FAS for usernames.
+# baseURL = "https://admin.fedoraproject.org/accounts"
+# fasuser = "elections"
+# faspw = "elections"
+# fas2_url = 'http://fakefas.fedoraproject.org/accounts/'
+fas2_url = app.config['FAS_BASE_URL']
+fas2_username = app.config['FAS_USERNAME']
+fas2_password = app.config['FAS_PASSWORD']
+fas2 = AccountSystem(fas2_url, username=fas2_username, password=fas2_password)
+
# modular imports
from fedora_elections import forms, models, redirect
@@ -55,20 +95,166 @@ def election_admin_required(f):
@app.route('/')
def index():
elections = models.Election.query.filter_by(frontpage=True).all()
- return flask.render_template('list/index.html', elections=elections)
+ return flask.render_template('list/index.html', elections=elections,
+ title="Elections")
+
+
+(a)app.route('/about/<election_alias>')
+def about_election(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/about.html', election=election,
+ usernamemap=usernamemap)
@app.route('/archive')
def archived_elections():
- raise NotImplementedError
+ now = datetime.utcnow()
+
+ try:
+ elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date<now, models.Election.id>0).all()
+ except NoResultFound:
+ flask.flash('There are no archived elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('election/archive.html', elections=elections)
+
+
+(a)app.route('/open')
+def open_elections():
+ now = datetime.utcnow()
+
+ try:
+ elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date>now,
+ models.Election.id>0).all()
+ except NoResultFound:
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ num_elections = models.Election.query.order_by(models.Election.start_date).\
+ filter(models.Election.end_date>now,
+ models.Election.id>0).count()
+ if (num_elections == 0):
+ flask.flash('There are no open elections.')
+ return redirect.safe_redirect_back()
+
+ return flask.render_template('list/index.html', elections=elections,
+ title='Open Elections')
### ELECTION VIEWS #############################################
-(a)app.route('/vote/<election_alias>')
+(a)app.route('/vote/<election_alias>', methods=['GET', 'POST'])
def vote(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).one()
- return flask.render_template('election/vote.html', election=election)
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'Pending':
+ flask.flash('Voting has not yet started, sorry.')
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Ended':
+ flask.flash('This election is closed. You have been redirected to ' +
+ 'the election results.')
+ return flask.redirect(flask.url_for('results', election.shortname))
+
+ votes = models.Vote.query.filter_by(election_id=election.id,
+ voter=flask.g.fas_user.username).count()
+ if (votes != 0):
+ flask.flash('You have already voted in the election!')
+ return redirect.safe_redirect_back()
+
+ num_candidates = election.candidates.count()
+
+ uvotes = {}
+ for candidate in election.candidates:
+ uvotes[candidate.id] = ""
+ next_action = 'vote'
+
+ if flask.request.method == 'POST':
+ next_action = ''
+ form_values = flask.request.form.values()
+ if 'Submit' in form_values:
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote >= 0 and vote <= num_candidates:
+ uvotes[candidate.id] = int(vote)
+ else:
+ flask.flash("Invalid Ballot!")
+ return redirect.safe_redirect_back()
+
+ except ValueError:
+ flask.flash("Invalid Ballot!")
+ return redirect.safe_redirect_back()
+
+ for uvote in uvotes:
+ new_vote = models.Vote(election_id=election.id,
+ voter=flask.g.fas_user.username,
+ timestamp=datetime.now(),
+ candidate_id=uvote,
+ value=uvotes[uvote] )
+ db.session.add(new_vote)
+ db.session.commit()
+
+ flask.flash("Your vote has been recorded. Thank you!")
+ return redirect.safe_redirect_back()
+
+ elif 'Preview' in form_values:
+ flask.flash("Please confirm your vote!")
+ for candidate in election.candidates:
+ try:
+ vote = int(flask.request.form[str(candidate.id)])
+ if vote > num_candidates:
+ flask.flash("Invalid data.")
+ uvotes[candidate.id] = 0
+ next_action = 'vote'
+ elif vote >= 0:
+ uvotes[candidate.id] = vote
+ else:
+ flask.flash("Invalid data2.")
+ uvotes[c.id] = 0
+ next_action = 'vote'
+ except ValueError:
+ flask.flash("Invalid data")
+
+ if (next_action != 'vote'):
+ next_action = 'confirm'
+
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/vote.html', election=election,
+ num_candidates=num_candidates,
+ usernamemap=usernamemap,
+ voteinfo=uvotes, nextaction=next_action)
### AUTH VIEWS #############################################
@@ -216,3 +402,54 @@ def admin_delete_candidate(election_alias, candidate_id):
election_alias=election.alias))
return flask.render_template('admin/delete_candidate.html', form=form,
candidate=candidate)
+
+
+(a)app.route('/results/<election_alias>')
+def election_results(election_alias):
+ try:
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ except NoResultFound:
+ flask.flash('The election, %s, does not exist.' % election_alias)
+ return redirect.safe_redirect_back()
+
+ if election.status == 'In progress':
+ flask.flash("We are sorry. The results for this election cannot be" \
+ " viewed at this time because the election is still in" \
+ " progress.")
+ return redirect.safe_redirect_back()
+
+ elif election.status == 'Pending':
+ flask.flash("We are sorry. The results for this election cannot be" \
+ " viewed at this time because the election has not" \
+ " started.")
+ return redirect.safe_redirect_back()
+
+ elif election.embargoed == 1:
+ if identity.in_group(config.get('admingroup', 'elections')):
+ pass
+ else :
+ match = 0
+ admingroups = models.ElectionAdminGroups.query.filter_by(
+ election_id=election.id).all()
+ for group in admingroups:
+ if identity.in_group(group.group_name):
+ match = 1
+
+ if match == 0:
+ flask.flash("We are sorry. The results for this election" \
+ "cannot be viewed because they are currently" \
+ " embargoed pending formal announcement.")
+ return redirect.safe_redirect_back()
+
+ usernamemap = {}
+ if (election.candidates_are_fasusers):
+ for candidate in election.candidates:
+ try:
+ usernamemap[candidate.id] = \
+ fas2.person_by_username(candidate.name)['human_name']
+ except (KeyError, AuthError):
+ # User has their name set to private or user doesn't exist.
+ usernamemap[candidate.id] = candidate.name
+
+ return flask.render_template('election/results.html', election=election,
+ usernamemap=usernamemap)
diff --git a/fedora_elections/images/border-left.png b/fedora_elections/images/border-left.png
new file mode 100644
index 0000000..6f256df
Binary files /dev/null and b/fedora_elections/images/border-left.png differ
diff --git a/fedora_elections/images/border-right.png b/fedora_elections/images/border-right.png
new file mode 100644
index 0000000..0e7365b
Binary files /dev/null and b/fedora_elections/images/border-right.png differ
diff --git a/fedora_elections/images/fedora-logo.png b/fedora_elections/images/fedora-logo.png
new file mode 100644
index 0000000..ce95893
Binary files /dev/null and b/fedora_elections/images/fedora-logo.png differ
diff --git a/fedora_elections/images/line.png b/fedora_elections/images/line.png
new file mode 100644
index 0000000..3498cb9
Binary files /dev/null and b/fedora_elections/images/line.png differ
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
index f025255..994f144 100644
--- a/fedora_elections/models.py
+++ b/fedora_elections/models.py
@@ -2,7 +2,6 @@ from fedora_elections import app, db
from datetime import datetime
-
class Election(db.Model):
__tablename__ = 'elections'
@@ -63,6 +62,11 @@ class Candidate(db.Model):
# candidates_are_fasusers
url = db.Column(db.Unicode(250))
+ @property
+ def vote_count(self):
+ values = [v.value for v in self.votes]
+ return sum(values)
+
class LegalVoter(db.Model):
__tablename__ = 'legalvoters'
diff --git a/fedora_elections/static/fedora.css b/fedora_elections/static/fedora.css
new file mode 100644
index 0000000..e56f85f
--- /dev/null
+++ b/fedora_elections/static/fedora.css
@@ -0,0 +1,1334 @@
+*
+{
+ margin: 0;
+ padding: 0;
+}
+
+html, body
+{
+ height: 100%;
+}
+
+body
+{
+ font-size: 76%;
+ background: #FFFFFF url(../images/border-left.png) 0 0 repeat-y;
+}
+
+a img
+{
+ border: none;
+}
+
+pre
+{
+ font-size: 2.6ex;
+ overflow: auto;
+}
+
+abbr, acronym {
+ border-bottom: 1px dotted #000000;
+ cursor: help;
+}
+
+#wrapper
+{
+ font: normal 1em/1.5 sans-serif;
+ margin-left: 18px;
+ min-height: 100%;
+ background: #FFFFFF url(../images/border-right.png) 100% 0 repeat-y;
+ padding-right: 18px;
+ overflow: hidden;
+}
+
+#head
+{
+ position: relative;
+ border-top: 10px solid #337ACC;
+ padding: 1ex 2ex 17px;
+ background: #FFFFFF url(../images/line.png) 0 100% repeat-x;
+}
+
+#head h1 a
+{
+ display: block;
+ text-indent: -9999px;
+ background: url(../images/fedora-logo.png) 20px 50% no-repeat;
+ height: 73px;
+ width: 175px;
+ overflow: hidden;
+ text-decoration: none;
+}
+
+#sidebar
+{
+ float: left;
+ width: 180px;
+ margin: 2ex 0;
+ background: #FFFFFF;
+ padding-bottom: 120px;
+}
+
+#banner
+{
+ text-align: center;
+}
+
+#banner img
+{
+ display: block;
+ width: 200px;
+ margin-left: 10px;
+}
+
+#nav
+{
+ margin: 0 1.5ex;
+ padding: 0 1.5ex;
+}
+
+#nav h2
+{
+ font-size: 0.9em;
+ border-bottom: 1px dotted #AAAAAA;
+ color: #444444;
+ margin: 2ex 0 1ex;
+ text-transform: uppercase;
+ font-weight: normal;
+}
+
+#nav ul
+{
+ list-style: url(../images/arrow.png);
+ margin-left: 15px;
+}
+
+#nav li
+{
+ margin: 0.25ex 1ex;
+ color: #777777;
+ font-size: 0.9em;
+}
+
+#nav a
+{
+ font-weight: bold;
+ margin-right: 0.75ex;
+ color: #729FCF;
+ font-size: 1.1em;
+ display: block;
+}
+
+.home #nav-home a, .get #nav-get a, .join #nav-join a, .help #nav-help a
+{
+ color: #777777;
+ text-decoration: none;
+}
+
+#content
+{
+ display: inline-block;
+ margin-left: 230px;
+ padding: 1.5ex 2ex 140px 0;
+ margin-bottom: 3ex;
+ color: #666666;
+ background: #FFFFFF;
+}
+
+#content p, #content ul, #content ol
+{
+ font-size: 1em;
+}
+
+/* Blame IE6 and the guillotine bug for this one */
+#content { display: block; }
+
+#content h2
+{
+ font-size: 1.6em;
+ padding-top: .75em;
+}
+
+#content h3
+{
+ color: #337acc;
+ margin: 1.5ex 0 0.5ex;
+ font-size: 1.4em;
+}
+
+#content h3.header {
+ display: block;
+}
+
+#content table
+{
+ border-collapse: collapse;
+}
+
+#content table th
+{
+ background: #DDDDDD;
+}
+
+#content table th, #content table td
+{
+ border: 1px solid #000000;
+ padding: 0.6ex;
+}
+
+#content p
+{
+ margin: 1ex 5ex 1ex 0;
+}
+
+#content ul, #content ol {
+ margin: 0 3ex 2ex;
+}
+
+#content ul
+{
+ list-style: square;
+}
+
+#content img
+{
+ margin: 2ex 0;
+}
+
+
+
+#content .download
+{
+ list-style-image: url(../images/arrow.png);
+ list-style-position: outside;
+ margin: 1.6ex 3ex;
+}
+
+#content .download li
+{
+ color: #777777;
+ font-size: 1.1em;
+ background: #EFEFEF;
+ padding: 1ex;
+ border-bottom: 1px dotted #AAAAAA;
+}
+
+#content .download a
+{
+ font-weight: bold;
+}
+
+#content .roles
+{
+ margin: 0!important;
+ list-style: none!important;
+ display: inline;
+}
+
+#content .roles li
+{
+ float: left;
+ width: 130px;
+ margin-bottom: 0.5ex;
+}
+
+#content .roles a
+{
+ line-height: 1;
+ height: 150px;
+ text-align: center;
+ display: block;
+ text-decoration: none;
+}
+
+#content .roles a:hover
+{
+ background-color: #EFEFEF;
+}
+
+#footer
+{
+ position: relative;
+ font: normal 0.8em/1.5 sans-serif;
+ text-align: center;
+ background: white url(../images/line-bottom.png) 0 0 repeat-x;
+ margin: -140px 0 0;
+ border-bottom: 10px solid #337ACC;
+ height: 130px;
+ color: #4D4D4D;
+}
+
+#bottom
+{
+ margin-left: 18px;
+ padding-right: 18px;
+}
+
+#footer ul
+{
+ list-style: none;
+ margin-bottom: .8em;
+}
+
+#footer li
+{
+ display: inline;
+ border-left: 1px solid #AAAAAA;
+ padding: 0 0.75ex;
+ margin-left: -1px;
+}
+
+#footer li.first
+{
+ border-left: none;
+}
+
+#footer a
+{
+ color: #3771C8;
+}
+
+#footer a:hover
+{
+ color: #112233;
+}
+
+#footer p
+{
+ margin: 0 1.5ex;
+}
+
+#footer .copy
+{
+}
+
+#footer .rh-logo {
+ padding-top: 3ex;
+ margin-bottom: 1em;
+}
+s
+#screenshot-banner {
+ text-align: left;
+}
+
+input, textarea, select
+{
+ border: #AAA 1px solid;
+}
+
+input, textarea, option
+{
+ padding: 2px 5px 2px 5px;
+}
+
+#content #sponsors
+{
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ overflow: auto;
+}
+
+#content #sponsors li
+{
+ text-align: center;
+ float: left;
+ height: 100px;
+ margin: 0 1ex;
+}
+
+#content #sponsors li img
+{
+ display: block;
+}
+
+#content #primary-sponsors
+{
+ margin: 0 0 4ex;
+ padding: 0;
+ overflow: auto;
+ border-bottom: 1px #AAAAAA solid;
+}
+
+#content #primary-sponsors .psponsor
+{
+ text-align: center;
+ margin: 0 1ex;
+}
+
+#content #primary-sponsors .psponsor td
+{
+ border-style: none;
+}
+
+#content #primary-sponsors .psponsor img
+{
+ display: block;
+ float: left;
+ margin-right: 6ex;
+}
+
+#content #primary-sponsors .psponsor p
+{
+ display: block;
+ text-align: left;
+}
+
+#content ul.downloadbox
+{
+ overflow: hidden;
+ list-style: none;
+ margin: 0 2ex;
+ padding: 6ex 2ex 4ex 180px;
+ border-radius: 1.5ex;
+ -webkit-border-radius: 1.5ex;
+ -moz-border-radius: 1.5ex;
+ background: #C0DBDD 30px 50% no-repeat;
+ font-size: 0.9em;
+ display: inline-block;
+}
+
+#content ul.installdvd { background-image: url(../images/installdvd.png); }
+#content ul.gnomelive { background-image: url(../images/gnomelive.png); }
+#content ul.kdelive { background-image: url(../images/kdelive.png); }
+
+.downloadbox li
+{
+ width: 32ex;
+ float: left;
+ padding: 0 0 2ex;
+}
+
+.downloadbox li li
+{
+ width: auto;
+ float: none;
+ padding: 0;
+}
+
+
+#content pre.command
+{
+ font-size: 1.3em;
+ border: solid #DDDDDD;
+ border-width: 1px 1px 1px 5px;
+ background: #F2F5F6;
+ padding: 0.5ex 2ex;
+}
+
+#content p.mini-note
+{
+ padding: 1ex 2ex;
+ border: 1px solid #DDDDDD;
+ background: #FFABAB;
+ font-size: .8em;
+ color: #4c4c4c;
+ -webkit-border-radius: 1.5ex;
+ -moz-border-radius: 1.5ex;
+ width: 80%
+}
+
+#content p.note
+{
+ padding: 1ex 2ex;
+ border: 1px solid #DDDDDD;
+ background: #F2F5F6;
+}
+
+#content p.warning
+{
+ padding: 1ex 2ex;
+ border: 1px solid #DDDDDD;
+ background: #FF6666;
+ color: #000000;
+}
+
+#content p.warning a
+{
+ color: #00154F;
+}
+#content ul#resources
+{
+ list-style: none;
+}
+
+#content ul#resources a
+{
+ padding-left: 20px;
+ background: url(../images/arrow.png) 0 50% no-repeat;
+}
+
+#content ul#resources li
+{
+ padding-left: 70px;
+ margin-bottom: 3.5ex;
+}
+
+ul#resources li.resource-docs
+{
+ background: url(../images/icon-docs.png) 0 50% no-repeat;
+}
+
+ul#resources li.resource-communicate
+{
+ background: url(../images/icon-communicate.png) 0 50% no-repeat;
+}
+
+ul#resources li.resource-download
+{
+ background: url(../images/icon-download.png) 0 50% no-repeat;
+}
+
+#content img.smirk
+{
+ margin: 0 1ex 0 0;
+}
+
+#content p.joinlink
+{
+ border: 1px solid #DDDDDD;
+ background: #EEEEEE;
+ padding: 1ex 5ex 1ex 5ex;
+ text-align: center;
+ font-size: 120%;
+ font-weight: bold;
+}
+
+#content #subprojects
+{
+ margin-right: 5ex;
+ padding-top: 1ex;
+}
+
+#content a:link
+{
+ color: #337ACC;
+ text-decoration: none;
+}
+
+#content a:visited
+{
+ color: #729FCF;
+ text-decoration: none;
+}
+
+/* get-fedora styles */
+
+.download-block .button:hover {
+ background-color: #9ECFFF !important;
+}
+
+.download-block {
+ margin-right: 23em;
+}
+
+.download-sidebar {
+ float: right;
+ width: 21em;
+ margin-left: 2em;
+}
+
+.download-sidebar a {
+ font-size: 1.5em;
+ display: block;
+ padding: 1em;
+ margin: 1em 0;
+}
+
+#other {
+ margin-top: 2ex;
+ background-color: #F0F0F0!important;
+}
+
+.download-block h3 {
+ margin-top: 0!important;
+}
+
+
+.download-block em.byline {
+ display: block;
+ color: #999999;
+ font-size: 0.8em;
+}
+
+.download-block em.download-tagline {
+ color: #999999;
+ font-size: 0.8em;
+}
+
+.download-block .button {
+ border-radius: 5px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ color: #FFFFFF!important;
+ text-decoration: none;
+ font-weight: bold;
+ line-height: 3;
+ margin: 0;
+ padding: 0.4em 0.5em;
+ border: 1px solid #446382;
+ background-color: #5F94C9 !important;
+ margin-top: 0.9em;
+}
+
+.big-button {
+ border-radius: 5px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ color: #FFFFFF!important;
+ text-decoration: none;
+ font-weight: bold;
+ line-height: 3;
+ margin: 0;
+ padding: 0.4em 0.5em;
+ background-color: #337acc !important;
+ margin-top: 0.9em;
+ font-size: 1.7em;
+}
+
+.big-button:hover {
+ background-color: #295596 !important;
+}
+
+
+.fine-print {
+ font-size: .9em !important;
+ color: #999999;
+ font-style: italic;
+}
+
+.resources-pane {
+ float: right;
+ background-color: #E5E5E5;
+ padding: 0.5em 1em;
+ border-radius: 5px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ margin-left: 1em;
+}
+
+.download-block p img {
+ float: left;
+ margin-right: 1.4em !important;
+}
+
+/** sidebar boxes for get-fedora **/
+
+.download-sidebar a {
+ display: block;
+ background: #337ACC url(../images/gradient_bg.png) repeat-x;
+ border-radius: 8px;
+ -webkit-border-radius: 8px;
+ -moz-border-radius: 8px;
+}
+
+.download-sidebar a.even {
+ background-color: #1855A2;
+}
+
+.download-sidebar a:hover {
+ background-color: #1158AA;
+}
+
+.download-sidebar a.even:hover {
+ background-color: #003380;
+}
+
+.download-sidebar a {
+ color: #FFFFFF!important;
+ font-size: medium;
+ text-decoration: none;
+ font-weight: bold;
+ line-height: 1.5;
+}
+
+.download-sidebar span#thereismorefedora a {
+ display: block;
+ background: none;
+ margin: 0px;
+ padding: 0px;
+}
+
+.download-sidebar span#thereismorefedora img {
+ margin: 0px;
+ padding: 0px;
+}
+
+#breadcrumbs {
+ border-bottom: 1px solid #DDDDDD;
+ margin-bottom: 1ex;
+ margin-right: 24%;
+}
+
+#breadcrumbs a {
+ text-decoration: none;
+ font-size: 1em;
+ color: #7788DD;
+}
+
+/** Web App Styles **/
+/* Links to other web apps */
+a.bodhi {
+ padding-left: 20px;
+ background: url(../images/icons/fedora-infra-icon_bodhi.png) 0 50% no-repeat;
+}
+
+a.koji {
+ padding-left: 20px;
+ background: url(../images/icons/fedora-infra-icon_koji.png) 0 50% no-repeat;
+}
+
+a.bugz {
+ padding-left: 20px;
+ background: url(../images/icons/fedora-infra-icon_bugzilla.png) 0 50% no-repeat;
+}
+
+a.pkgdb {
+ padding-left: 20px;
+ background: url(../images/icons/fedora-infra-icon_pkgdb.png) 0 50% no-repeat;
+}
+
+a.cvs {
+ padding-left: 20px;
+ background: url(../images/icons/fedora-infra-icon_source-control.png) 0 50% no-repeat;
+}
+
+/* tooltip */
+.tooltip {
+ display: block;
+ position: absolute;
+ border: 1px solid black;
+ background-color: #ffffdd;
+ width: 200px;
+}
+
+/* style to hide an element */
+.invisible {
+ display: none;
+}
+
+/* styles for tg flash messages */
+.flash {
+ background: #DEE681;
+ border: 1px solid #CCBBAA;
+ padding: 1.5ex 15px 1.5ex 43px;
+ margin: 1ex 0;
+}
+
+.flash .success {
+ background: #DEE681 url(../images/success.png) 10px 50% no-repeat;
+}
+
+.flash .warning {
+ background: #DEE681 url(../images/important.png) 10px 50% no-repeat;
+}
+
+.flash .info {
+ background: #DEE681 url(../images/info.png) 10px 50% no-repeat;
+}
+
+/* toolbar styles */
+.toolbar {
+ list-style: none;
+ list-style-image: none;
+ list-style-position: outside;
+ list-style-type: none;
+ text-align: right;
+ color: #888888;
+ font-size: 0.9em;
+ text-transform: uppercase;
+ vertical-align: middle;
+}
+
+.toolbar * {
+ display: inline;
+}
+
+#head #main-toolbar {
+ float: right;
+ margin-top: 9px;
+ width: 50%;
+}
+
+.toolbar input.button {
+ background-color: #888888;
+ color: white;
+ font-size: 0.9em;
+ margin-left: 5px;
+ padding: 2px;
+ text-transform: uppercase;
+ vertical-align: middle;
+}
+
+/* panel styles */
+#content .panel {
+ border: medium none;
+ margin-top: 12px;
+}
+
+#content .panel h3 {
+ -moz-border-radius-bottomleft: 0;
+ -moz-border-radius-bottomright: 0;
+ -moz-border-radius-topleft: 12px;
+ -moz-border-radius-topright: 12px;
+ background-color: #D2D2D2;
+ color: white;
+ font-size: 1em;
+ padding: 8px 6px 6px;
+ text-transform: uppercase;
+ margin: 0;
+}
+
+/* Login panel */
+#login-box form .field {
+ margin: 0 0 1ex;
+ overflow: hidden;
+ text-align: left;
+}
+
+#login-box .field label {
+ clear: left;
+ float:left;
+ padding: 0 2ex 0 0;
+ text-align: left;
+ width: 32ex;
+}
+
+#content .login {
+ -moz-border-radius-bottomleft: 10px;
+ -moz-border-radius-bottomright: 10px;
+ -moz-border-radius-topleft: 10px;
+ -moz-border-radius-topright: 10px;
+ background-color: #5F94C9;
+ color: white;
+ padding: 8px 12px;
+}
+
+#content div.login * {
+ color: white;
+ padding: 0;
+ margin: 0;
+}
+
+#content .login h3 {
+ background-color: #5F94C9;
+}
+
+#content .login input {
+ padding: 2px 5px;
+ margin-bottom: 5px;
+ color: black;
+}
+
+#content .login label {
+ color: white;
+ display: block;
+ float: left;
+ font-size: 0.8em;
+ margin-right: 10px;
+ width: 60px;
+ text-align: left;
+}
+
+#content .login .button {
+ background-color: #777777;
+ border: 1px solid #555555;
+ color: white;
+}
+
+#content .login ul {
+ list-style-position: inside;
+}
+
+#export-regulations {
+ padding-top: 1em;
+ margin-left: 2em;
+}
+
+#export-regulations .read-more img, #export-regulations .details img {
+ margin: 0;
+}
+
+#export-regulations .details {
+ display: inline;
+}
+
+#export-regulations a {
+ text-decoration: none;
+}
+
+.read-more, .re-collapse {
+ float:right;
+}
+
+.read-more img {
+ float: none;
+}
+
+/* applies to get-* pages */
+
+#download-resources {
+ float:right;
+ margin-left:.5em;
+ width:21em; /* tofix */
+}
+
+#download-resources h4 {
+ text-transform: uppercase;
+ margin-bottom: .5em;
+}
+
+#download-resources li {
+ padding: 3px 0;
+}
+
+#download-resources ul.checkmark-list {
+ list-style: disc url("/static/images/icon-checkmark.png") outside;
+}
+
+#download-resources li.guide-list {
+ list-style: disc url("/static/images/icon-guide.png") outside;
+}
+
+#download-resources li.webpage-list {
+ list-style: disc url("/static/images/icon-webpage.png") outside;
+}
+
+#download-resources li.support-list {
+ list-style: disc url("/static/images/icon-support.png") outside;
+}
+
+#download-resources li.verify-list {
+ list-style: disc url("/static/images/icon-signed-verified.png") outside;
+}
+
+#download-resources div, #download-resources div h4 {
+ padding: 5px 0 5px 0;
+}
+
+#download-resources div p {
+ padding-left: 5px;
+}
+
+#download-resources p,
+#download-resources ul {
+ margin-bottom: 2em;
+}
+
+#download-resources ul {
+ margin-left: 2em;
+}
+
+#download-options-area {
+ background-color: #f2f2f2;
+ padding: 15px;
+}
+
+#download-options-area h4 {
+ color: #337ACC;
+}
+
+.other-options {
+ float: right;
+ border-radius: 5px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ margin-left: 1em;
+ width: 45%;
+}
+
+.upgrade-section {
+ width: 50%;
+}
+
+/* get-* format tab */
+.format-arch {
+ border-bottom: 1px #E5E5E5 solid;
+ float:left;
+ width: 90%;
+ padding: 5px;
+}
+
+.support-icon {
+ border: 0px;
+ padding: 3px;
+}
+
+.vendor-header {
+ clear: left;
+ font-size: 1.3em;
+ padding-top: 15px;
+ padding-bottom: 15px;
+}
+
+.freemedia-header {
+ clear: left;
+ font-size: 1.3em;
+ padding-top: 15px;
+}
+
+
+.vendor1, .vendor2 {
+ width: 50%;
+}
+
+/* end: applies to get-* pages */
+
+/* tabs */
+/* root element for tabs */
+ul.tabs {
+ margin: 0 !important;
+ padding: 0;
+ height: 30px;
+ border-bottom: 1px solid #dcdcdc;
+ width: 70%;
+}
+
+/* single tab */
+ul.tabs li {
+ float: left;
+ padding: 0;
+ margin: 0;
+ list-style-type: none;
+}
+
+/* link inside the tab. no background image */
+ul.tabs a {
+ float: left;
+ font-size: 13px;
+ display: block;
+ padding: 5px 10px;
+ text-decoration: none;
+ border: 1px solid #dcdcdc;
+ border-bottom: 0px;
+ height: 18px;
+ background-color: #dcdcdc;
+ color: #3771c8;
+ margin-right: 4px;
+ -moz-border-radius-topleft: 4px;
+ -moz-border-radius-topright: 4px;
+ position: relative;
+ top: 1px;
+ text-transform: uppercase;
+ font-weight: bold;
+}
+
+ul.tabs a:hover {
+ background-color: #fff;
+ color: #818181;
+ text-transform: uppercase;
+ font-weight: bold;
+}
+
+/* selected tab */
+ul.tabs a.current {
+ background-color: #fff;
+ border-bottom: 2px solid #fff;
+ color: #808080;
+ cursor: default;
+ text-transform: uppercase;
+ font-weight: bold;
+}
+
+/* tab pane */
+div.panes div {
+ display: none;
+ border: 0px;
+ border-left: 1px solid #dcdcdc;
+ min-height: 150px;
+ padding: 15px 10px;
+ /* tofix width: 70%; */
+ float: left;
+}
+
+#content a.current:link {
+ color: #828282;
+}
+/* end: tabs */
+
+/* --- */
+#content span.download-opt1
+{
+ float: left;
+ width: 45%;
+}
+
+div.panes span.download-option {
+ display: block;
+ /* tofix: width: 300px !important; */
+ width: 50%;
+}
+
+div.panes span.opt1 { float: left; }
+div.panes span.opt2 {
+ float: right;
+ width: 50%;
+}
+
+#export-regulations {
+ clear: both;
+ margin-bottom: 2em;
+}
+
+/* Hack to make it simple to get rid of borders in a table (ianweller) */
+#content table.nobordersplz th, #content table.nobordersplz td { border-width: 0px; }
+
+/* get-option common */
+/* keeps common stuff in get pages */
+.star-fedora-default, .star-most-compatible {
+ background-color: #F7FF9D;
+ border: 1px solid #A19800;
+ padding: 0.2em;
+ font-weight: bold;
+ color: #A19800;
+ font-size: 0.8em;
+ margin: 7px;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+.download-button-mini {
+ border-radius: 5px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ color: #FFFFFF!important;
+ text-decoration: none;
+ font-weight: bold;
+ line-height: 2;
+ font-size:0.85em;
+ padding: 5px;
+ background-color: #337ACC !important;
+ white-space: nowrap;
+}
+
+.download-button-mini:hover {
+ background-color: #295596 !important;
+}
+
+.opt1 { float: left; }
+.opt2 { float: right; }
+
+#content .format-arch .opt1 p, #content .format-arch .opt1 h3, #content .format-arch .opt2 p, #content .format-arch .opt2 h3 {
+ padding: 0px;
+ margin: 0px
+}
+
+.panes {
+ width: 68%
+}
+
+.shownojs { display: none; }
+
+.area-callout
+{
+ display: inline-block; /* to put icons next to header */
+}
+
+/* end: get-option common */
+
+/* get-option - desktop pane */
+#content .desktop-section table, #content .desktop-section table td {
+ border: 0px;
+}
+
+.desktop-section ul {
+ float: left;
+}
+
+.desktop-section img.desktop-screenshot {
+ float: right;
+}
+
+.desktop-section table tbody tr {
+ background-color: #fff;
+}
+
+.desktop-section td.description {
+ max-width: 230px;
+}
+
+.desktop-section td {
+ vertical-align: top;
+}
+
+img.desktop-screenshot:hover {
+ cursor: pointer;
+ cursor: hand;
+}
+
+/* end: get-option - desktop pane */
+
+
+/* get-option - architecture pane */
+.arch-section, .desktop-section {
+ clear:left;
+ display:block;
+ margin-bottom:1em;
+ padding:0.5em 1em;
+}
+
+img.download-media
+{
+ float: left;
+ padding-right: 7px;
+}
+
+.arch-sparc-area {
+ background-color: #f5f5f5;
+ padding: 1em;
+ clear: both;
+ display: block;
+}
+
+.arch-section h3 span {
+ color: black;
+}
+
+/* end: get-option - architecture pane */
+
+.panes table tr {
+ background-color:#F2F2F2;
+ border:0 none;
+}
+
+#get-acitivites td.rank {
+ background-color:white;
+ font-weight:bold;
+ padding-left:0;
+ padding-right:7px;
+ text-align:left;
+}
+
+#get-acitivites td.name {
+ font-size:xx-small;
+ font-style:italic;
+}
+
+#get-acitivites table td {
+ border-color: white !important;
+ border-style: solid !important;
+ border-width: 0 0 18px !important;
+ padding: 0.8em;
+ vertical-align: top;
+}
+
+.learn-more {
+ display: none;
+}
+
+/*
+ * note: probably not needed
+#content table th, #content table td {
+ border:1px solid #000000;
+ padding:0.6ex;
+}
+*/
+
+/* end: get-option - activites pane */
+
+#content img {
+ margin:0;
+}
+
+
+
+/* make it work in 1024x786 */
+
+#desktop-versions {
+ width: 100%;
+}
+
+.panes {
+ min-width: 50%;
+}
+
+.panes table {
+ width: 80%;
+}
+
+#download-resources {
+ min-width: 200px;
+ width: 20%;
+}
+
+
+#content .panes {
+ min-width: 400px;
+}
+
+#nav {
+ margin: 0 1em;
+ padding:0 0 0 1.5em;
+}
+
+tr.dl-buttons td {
+ min-width: 104px !important;
+}
+
+div.panes div {
+ background-color: transparent !important;
+ padding-right: 15 5 15 10;
+}
+
+.arch-section,
+.desktop-section {
+ padding-right: 0.5em 0 0.5em 1em;
+}
+
+/* the overlayed element */
+.simple_overlay {
+
+ /* must be initially hidden */
+ display:none;
+
+ /* place overlay on top of other elements */
+ z-index:10000;
+
+ /* styling */
+ background-color:#333;
+
+ /* disabled for now - so it fits around images
+ * width:675px;
+ */
+ min-height:200px;
+ border:1px solid #666;
+
+ /* CSS3 styling for latest browsers */
+ -moz-box-shadow:0 0 90px 5px #000;
+ -webkit-box-shadow: 0 0 90px #000;
+}
+
+/* close button positioned on upper right corner */
+.simple_overlay .close {
+ background-image:url(../images/overlay-close.png);
+ position:absolute;
+ right:-15px;
+ top:-15px;
+ cursor:pointer;
+ height:35px;
+ width:35px;
+}
+
+/* styling for elements inside overlay */
+.overlay_details {
+ position:absolute;
+ top:15px;
+ right:15px;
+ font-size:11px;
+ color:#fff;
+ width:150px;
+}
+
+.overlay_details h3 {
+ color:#aba;
+ font-size:15px;
+ margin:0 0 -10px 0;
+}
+
+/* end: the overlayed element */
+
+.global-notice {
+ background-color: #f6f6f6;
+ border: 1px solid #ddd;
+ padding: 0.5em 1em;
+}
diff --git a/fedora_elections/static/style.css b/fedora_elections/static/style.css
index b316ea7..8c4a9b3 100644
--- a/fedora_elections/static/style.css
+++ b/fedora_elections/static/style.css
@@ -1,90 +1,144 @@
body {
- font-family: sans-serif;
- font-size: 12pt;
- line-height: 1.35;
+ font-family: sans-serif;
+ font-size: 12pt;
+ line-height: 1.35;
}
#flashes {
- background-color: #ffa;
+ background-color: #ffa;
}
nav {
- background-color: #eee;
- padding: 0.5em 1em;
+ background-color: #eee;
+ padding: 0.5em 1em;
}
nav h2 {
- display: inline;
- font-size: inherit;
- margin-right: 1em;
+ display: inline;
+ font-size: inherit;
+ margin-right: 1em;
}
nav ul {
- display: inline;
- list-style: none;
- padding: 0;
+ display: inline;
+ list-style: none;
+ padding: 0;
}
nav li {
- display: inline;
- margin-right: .5em;
+ display: inline;
+ margin-right: .5em;
}
.clear {
- clear: both;
+ clear: both;
}
/* input elements */
fieldset {
- border: 0;
- padding: 0;
- margin: .5em 0;
- float: left;
- clear: both;
+ border: 0;
+ padding: 0;
+ margin: .5em 0;
+ float: left;
+ clear: both;
}
.row {
- margin: .25em 0;
+ margin: .25em 0;
}
.row label:first-child {
- width: 8em;
- margin-right: 0.75em;
- text-align: right;
- clear: both;
- font-weight: bold;
- display: inline-block;
- vertical-align: top;
+ width: 8em;
+ margin-right: 0.75em;
+ text-align: right;
+ clear: both;
+ font-weight: bold;
+ display: inline-block;
+ vertical-align: top;
}
.row textarea {
- width: 30em;
- height: 8em;
+ width: 30em;
+ height: 8em;
}
form .row .errors {
- list-style: none;
- margin: .5em 0 1em 10.9375em;
- padding: 0;
- font-size: .8em;
- color: #c00;
- width: 30em;
+ list-style: none;
+ margin: .5em 0 1em 10.9375em;
+ padding: 0;
+ font-size: .8em;
+ color: #c00;
+ width: 30em;
}
div.help {
- width: 40%;
- float: right;
- background-color: #ffa;
- border-top: 1px solid #ee9;
- padding: 1em;
- font-size: .8em;
- margin: .625em 0;
+ width: 40%;
+ float: right;
+ background-color: #ffa;
+ border-top: 1px solid #ee9;
+ padding: 1em;
+ font-size: .8em;
+ margin: .625em 0;
}
div > *:first-child { margin-top: 0; }
div > *:last-child { margin-bottom: 0; }
form .buttons.indent {
- margin-left: 8.75em;
+ margin-left: 8.75em;
+}
+
+.vthot
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #9C0000;
+}
+
+.vtwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #FFA300;
+}
+
+.vtmedwarm
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #93FF63;
+}
+
+.vtmedcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #00DAFE;
+}
+
+.vtcool
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #000000;
+ background: #0033FF;
+}
+
+.vtcold
+{
+ border: 1px solid #DDDDDD;
+ padding: 1.5ex;
+ margin: 1ex 0;
+ color: #FFFFFF;
+ background: #000083;
}
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index e40f882..fafefee 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -1,21 +1,26 @@
{% extends "base.html" %}
{% block title %}Election administration{% endblock %}
{% block body %}
-<h2>Election administration</h2>
-<table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- </tr>
- {% for election in elections %}
- <tr>
- <td><a href="{{ url_for('admin_view_election', election_alias=election.alias) }}">{{ election.summary }}</a></td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- </tr>
- {% endfor %}
-</table>
-{% endblock %}
+ <h2>Election administration</h2>
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>
+ <a href="{{ url_for('admin_view_election',
+ election_alias=election.alias) }}">
+ {{ election.summary }}
+ </a>
+ </td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+{%endblock %}
diff --git a/fedora_elections/templates/admin/candidate.html b/fedora_elections/templates/admin/candidate.html
index 9546da0..a7b2fda 100644
--- a/fedora_elections/templates/admin/candidate.html
+++ b/fedora_elections/templates/admin/candidate.html
@@ -2,16 +2,18 @@
{% from "_formhelpers.html" import render_field %}
{% block title %}{{ submit_text }}{% endblock %}
{% block body %}
-<h2>{{ submit_text }}</h2>
+ <h2>{{ submit_text }}</h2>
-<form action="" method="post">
- <fieldset>
- {{ render_field(form.name) }}
- {{ render_field(form.url) }}
- </fieldset>
+ <form action="" method="post">
+ <fieldset>
+ {{ render_field(form.name) }}
+ {{ render_field(form.url) }}
+ </fieldset>
- <br class="clear">
- <p class="buttons"><input type="submit" class="submit positive" value="{{ submit_text }}"></p>
- {{ form.csrf_token }}
-</form>
+ <br class="clear">
+ <p class="buttons">
+ <input type="submit" class="submit positive" value="{{ submit_text }}">
+ </p>
+ {{ form.csrf_token }}
+ </form>
{% endblock %}
diff --git a/fedora_elections/templates/admin/delete_candidate.html b/fedora_elections/templates/admin/delete_candidate.html
index 3cfe4be..d764f19 100644
--- a/fedora_elections/templates/admin/delete_candidate.html
+++ b/fedora_elections/templates/admin/delete_candidate.html
@@ -2,12 +2,17 @@
{% from "_formhelpers.html" import render_field %}
{% block title %}Delete candidate{% endblock %}
{% block body %}
-<h2>{{ self.title() }}</h2>
+ <h2>{{ self.title() }}</h2>
-<form action="" method="post">
- <p>Are you sure you want to delete candidate "{{ candidate.name }}"?</p>
- <p class="buttons"><input type="submit" value="Delete candidate">
- <a href="{{ url_for('admin_view_election', election_alias=candidate.election.alias) }}">Cancel</a></p>
- {{ form.csrf_token }}
-</form>
+ <form action="" method="post">
+ <p>Are you sure you want to delete candidate "{{ candidate.name }}"?</p>
+ <p class="buttons">
+ <input type="submit" value="Delete candidate">
+ <a href="{{ url_for('admin_view_election',
+ election_alias=candidate.election.alias) }}">
+ Cancel
+ </a>
+ </p>
+ {{ form.csrf_token }}
+ </form>
{% endblock %}
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
index e9d05c7..6e2a3d6 100644
--- a/fedora_elections/templates/admin/election_form.html
+++ b/fedora_elections/templates/admin/election_form.html
@@ -2,61 +2,76 @@
{% from "_formhelpers.html" import render_field %}
{% block title %}{{ submit_text }}{% endblock %}
{% block body %}
-<h2>{{ self.title() }}</h2>
+ <h2>{{ self.title() }}</h2>
-<form action="" method="post">
- <fieldset>
- {{ render_field(form.summary) }}
- </fieldset>
- <div class="help">
- <p>This is the name of the election that users will first see.</p>
- <p>Example: <kbd>Fedora Board, November 2010</kbd></p>
- </div>
- <fieldset>
- {{ render_field(form.alias) }}
- </fieldset>
- <div class="help">
- <p>A short, unique name that will be used for the election's URL. Lowercase letters, numbers, hyphens and underscores only.</p>
- <p>Example: <kbd>boardf15</kbd></p>
- </div>
- <fieldset>
- {{ render_field(form.description) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.url) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.start_date, " at 00:00:00 UTC") }}
- {{ render_field(form.end_date, " at 23:59:59 UTC") }}
- </fieldset>
- <div class="help">
- <p>Enter dates in <kbd>YYYY-MM-DD</kbd> format.</p>
- </div>
- <fieldset>
- {{ render_field(form.number_elected) }}
- </fieldset>
- <div class="help">
- <p>This determines the cutoff for how many candidates win the election. For example, if you are electing four empty seats on a committee, enter <kbd>4</kbd> here.</p>
- </div>
- <fieldset>
- {{ render_field(form.candidates_are_fasusers) }}
- </fieldset>
- <div class="help">
- <p>Check this if every candidate in this election has a FAS user account. This will automatically pull full names and wiki page URLs from FAS.</p>
- </div>
- <fieldset>
- {{ render_field(form.frontpage) }}
- </fieldset>
- <fieldset>
- {{ render_field(form.embargoed) }}
- </fieldset>
- <div class="help">
- <p>Embargo results when the election is finished. To unembargo them, edit the election and uncheck this box.</p>
- </div>
+ <form action="" method="post">
+ <fieldset>
+ {{ render_field(form.summary) }}
+ </fieldset>
+ <div class="help">
+ <p>This is the name of the election that users will first see.</p>
+ <p>Example: <kbd>Fedora Board, November 2010</kbd></p>
+ </div>
+ <fieldset>
+ {{ render_field(form.alias) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ A short, unique name that will be used for the election's URL.
+ Lowercase letters, numbers, hyphens and underscores only.
+ </p>
+ <p>Example: <kbd>boardf15</kbd></p>
+ </div>
+ <fieldset>
+ {{ render_field(form.description) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.url) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.start_date, " at 00:00:00 UTC") }}
+ {{ render_field(form.end_date, " at 23:59:59 UTC") }}
+ </fieldset>
+ <div class="help">
+ <p>Enter dates in <kbd>YYYY-MM-DD</kbd> format.</p>
+ </div>
+ <fieldset>
+ {{ render_field(form.number_elected) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ This determines the cutoff for how many candidates win the election.
+ For example, if you are electing four empty seats on a committee,
+ enter <kbd>4</kbd> here.
+ </p>
+ </div>
+ <fieldset>
+ {{ render_field(form.candidates_are_fasusers) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ Check this if every candidate in this election has a FAS user account.
+ This will automatically pull full names and wiki page URLs from FAS.
+ </p>
+ </div>
+ <fieldset>
+ {{ render_field(form.frontpage) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.embargoed) }}
+ </fieldset>
+ <div class="help">
+ <p>
+ Embargo results when the election is finished. To unembargo them, edit
+ the election and uncheck this box.
+ </p>
+ </div>
- <br class="clear">
- {% block election_form_extra %}{% endblock %}
- <p class="buttons"><input type="submit" class="submit positive" value="{{ submit_text }}"></p>
- {{ form.csrf_token }}
-</form>
+ <br class="clear">
+ {% block election_form_extra %}{% endblock %}
+ <p class="buttons">
+ <input type="submit" class="submit positive" value="Save Changes">
+ </p>
+ {{ form.csrf_token }}
+ </form>
{% endblock %}
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
index 4b5b174..ed070f3 100644
--- a/fedora_elections/templates/admin/view_election.html
+++ b/fedora_elections/templates/admin/view_election.html
@@ -1,26 +1,42 @@
{% extends "base.html" %}
{% block body %}
-<h2>{{ election.summary }}</h2>
-<p>{{ election.description }}</p>
-<p><a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">Edit election</a>
-<ul>
- <li>On front page: {{ election.frontpage }}</li>
- <li>Start date: {{ election.start_date }}</li>
- <li>End date: {{ election.end_date }}</li>
- <li>Number elected: {{ election.number_elected }}</li>
- <li>Election type: {{ election.voting_type }}</li>
-</ul>
-<h3>Candidates</h3>
-{% if election.candidates.all() %}
-<ul>
- {% for candidate in election.candidates.all() %}
- <li>{{ candidate.name }}
- <a href="{{ url_for('admin_edit_candidate', election_alias=election.alias, candidate_id=candidate.id) }}">[edit]</a>
- <a href="{{ url_for('admin_delete_candidate', election_alias=election.alias, candidate_id=candidate.id) }}">[x]</a></li>
- {% endfor %}
-</ul>
-{% else %}
-<p>There are no candidates.</p>
-{% endif %}
-<a href="{{ url_for('admin_add_candidate', election_alias=election.alias) }}">Add a candidate</a>
+ <h2>{{ election.summary }}</h2>
+ <p>{{ election.description }}</p>
+ <p>
+ <a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">
+ Edit election
+ </a>
+ </p>
+ <ul>
+ <li>On front page: {{ election.frontpage }}</li>
+ <li>Start date: {{ election.start_date }}</li>
+ <li>End date: {{ election.end_date }}</li>
+ <li>Number elected: {{ election.number_elected }}</li>
+ <li>Election type: {{ election.voting_type }}</li>
+ </ul>
+ <h3>Candidates</h3>
+ {% if election.candidates.all() %}
+ <ul>
+ {% for candidate in election.candidates.all() %}
+ <li>{{ candidate.name }}
+ <a href="{{ url_for('admin_edit_candidate',
+ election_alias=election.alias,
+ candidate_id=candidate.id) }}">
+ [edit]
+ </a>
+ <a href="{{ url_for('admin_delete_candidate',
+ election_alias=election.alias,
+ candidate_id=candidate.id) }}">
+ [x]
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>There are no candidates.</p>
+ {% endif %}
+ <a href="{{ url_for('admin_add_candidate',
+ election_alias=election.alias) }}">
+ Add a candidate
+ </a>
{% endblock %}
diff --git a/fedora_elections/templates/auth/login.html b/fedora_elections/templates/auth/login.html
index aafe3a8..9e7b73c 100644
--- a/fedora_elections/templates/auth/login.html
+++ b/fedora_elections/templates/auth/login.html
@@ -2,13 +2,19 @@
{% from "_formhelpers.html" import render_field %}
{% block title %}Log in{% endblock %}
{% block body %}
-<h2>Log in</h2>
+ <h2>Log in</h2>
-<form action="" method="post">
- <p>Log in using your <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a> account.</p>
- {{ render_field(form.username) }}
- {{ render_field(form.password) }}
- <p class="buttons indent"><input type="submit" class="submit positive" value="Log in"></p>
- {{ form.csrf_token }}
-</form>
+ <form action="" method="post">
+ <p>
+ Log in using your
+ <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a>
+ account.
+ </p>
+ {{ render_field(form.username) }}
+ {{ render_field(form.password) }}
+ <p class="buttons indent"><input type="submit" class="submit positive"
+ value="Log in">
+ </p>
+ {{ form.csrf_token }}
+ </form>
{% endblock %}
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
index b167ba4..3e6f806 100644
--- a/fedora_elections/templates/base.html
+++ b/fedora_elections/templates/base.html
@@ -1,42 +1,134 @@
<!DOCTYPE html>
<html>
- <head>
- {% block head %}
- <title>Fedora Elections{% if self.title() %} — {% block title %}{% endblock %}{% endif %}</title>
- <link rel="stylesheet" href="{{ url_for("static", filename="style.css") }}">
- {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
- {% endfor %}
- {% endblock %}
- </head>
- <body>
- <h1>Fedora Elections</h1>
- {% if g.fas_user %}
- <p>Hello, {{ g.fas_user.username }} — <a href="{{ url_for("auth_logout") }}">log out</a></p>
- {% else %}
- <p>You are not logged in — <a href="{{ url_for("auth_login") }}">log in</a></p>
- {% endif %}
- <nav>
- <h2>Elections</h2>
- <ul>
- <li><a href="{{ url_for("index") }}">Open elections</a></li>
- <li><a href="{{ url_for("archived_elections") }}">Archived elections</a></li>
- </ul>
- <br>
- <h2>Administration</h2>
- <ul>
- <li><a href="{{ url_for("admin_view_elections") }}">My elections</a></li>
- <li><a href="{{ url_for("admin_new_election") }}">Create new election</a></li>
- </ul>
- </nav>
- {% with messages = get_flashed_messages() %}
- {% if messages %}
- <ul id="flashes">
- {% for message in messages %}
- <li>{{ message }}</li>
- {% endfor %}
- </ul>
- {% endif %}
- {% endwith %}
- {% block body %}{% endblock %}
- </body>
+ <head>
+ {% block head %}
+ <title>
+ Fedora Elections
+ {% if self.title() %}
+ —
+ {% block title %}{% endblock %}
+ {% endif %}
+ </title>
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="{{ url_for("static", filename="style.css") }}" />
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="https://fedoraproject.org/static/css/fedora.css" />
+ {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
+ {% endfor %}
+ {% endblock %}
+ </head>
+ <body>
+ <div id="wrapper">
+ <div id="head">
+ <h1><a href="https://admin.fedoraproject.org/en/index">Fedora</a></h1>
+ </div>
+
+ <div id="sidebar">
+ <div id="nav">
+ <h2>Navigation</h2>
+ <ul>
+ <li id="nav-home">
+ <a href="http://fedoraproject.org/en/index">Home</a>
+ </li>
+ <li id="nav-get">
+ <a href="http://fedoraproject.org/en/get-fedora">Get Fedora</a>
+ </li>
+ <li id="nav-join">
+ <a href="http://fedoraproject.org/en/join-fedora">Join Fedora</a>
+ </li>
+ <li id="nav-help">
+ <a href="http://fedoraproject.org/en/get-help">Get Help</a>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <div id="content">
+ <ul class="toolbar" id="#main-toolbar">
+ {% if g.fas_user %}
+ <li>
+ Hello, {{ g.fas_user.username }} —
+ <a href="{{ url_for("auth_logout") }}">log out</a>
+ </li>
+ {% else %}
+ <li>
+ You are not logged in —
+ <a href="{{ url_for("auth_login") }}">log in</a>
+ </li>
+ {% endif %}
+ </ul>
+ <h2>Elections</h2>
+ <ul>
+ <li>
+ <a href="{{ url_for("open_elections") }}">
+ Open elections
+ </a>
+ </li>
+ <li>
+ <a href="{{ url_for("archived_elections") }}">
+ Archived elections
+ </a>
+ </li>
+ </ul>
+ <br>
+ <h2>Administration</h2>
+ <ul>
+ <li>
+ <a href="{{ url_for("admin_view_elections") }}">
+ My elections
+ </a>
+ </li>
+ <li>
+ <a href="{{ url_for("admin_new_election") }}">
+ Create new election
+ </a>
+ </li>
+ </ul>
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+ <ul id="flashes">
+ {% for message in messages %}
+ <li>{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {% endwith %}
+ {% block body %}
+ {% endblock %}
+ </div>
+ </div>
+ <div id="bottom">
+ <div id="footer">
+ {% block footer %}
+ <p class="copy">
+ Copyright © 2008 Red Hat, Inc. and others. Please send any
+ comments or corrections to the
+ <a href="mailto:webmaster@fedoraproject.org">
+ websites team
+ </a>.
+ </p>
+ <p class="disclaimer">
+ The Fedora Project is maintained and driven by the community and
+ sponsored by Red Hat. This is a community maintained site. Red
+ Hat is not responsible for content.
+ </p>
+ <ul>
+ <li class="first">
+ <a href="/en/sponsors">Sponsors</a>
+ </li>
+ <li>
+ <a href="http://fedoraproject.org/wiki/Legal">
+ Legal
+ </a>
+ </li>
+ <li>
+ <a href="http://fedoraproject.org/wiki/Legal/TrademarkGuidelines">
+ Trademark Guidelines
+ </a>
+ </li>
+ </ul>
+ </div>
+ {% endblock %}
+ </div>
+ </body>
</html>
diff --git a/fedora_elections/templates/election/about.html b/fedora_elections/templates/election/about.html
new file mode 100644
index 0000000..c7bf0ec
--- /dev/null
+++ b/fedora_elections/templates/election/about.html
@@ -0,0 +1,40 @@
+{% extends "base.html" %}
+{% block title %}Election Information{% endblock %}
+{% block body %}
+ <h1>{{ election.summary }}</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ {% if election.status == 'Ended' %}
+ <a href="/results/{{ election.alias }}">Show Results!</a>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% if election.legal_voters.count() != 0 %}
+ <p>
+ To vote in this election you must be a member of any one of the following groups:
+ </p>
+ <ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p />
+ {% endif %}
+{% endblock %}
diff --git a/fedora_elections/templates/election/archive.html b/fedora_elections/templates/election/archive.html
new file mode 100644
index 0000000..ad35d6b
--- /dev/null
+++ b/fedora_elections/templates/election/archive.html
@@ -0,0 +1,28 @@
+{% extends "base.html" %}
+{% block body %}
+
+ <h2>Past Elections</h2>
+
+ <table border="1" cellpadding="1">
+ <tr>
+ <th></th>
+ <th>Start Date (UTC)</th>
+ <th>End Date (UTC)</th>
+ <th></th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{election.description}}</td>
+ <td>{{election.start_date}}</td>
+ <td>{{election.end_date}}</td>
+ <td>
+ <a href="about/{{ election.alias }}"">More information</a>
+ </td>
+ <td>
+ <a href="results/{{ election.alias }}">Results</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+{% endblock %}
diff --git a/fedora_elections/templates/election/results.html b/fedora_elections/templates/election/results.html
new file mode 100644
index 0000000..f61d214
--- /dev/null
+++ b/fedora_elections/templates/election/results.html
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+{% block body %}
+ <h1>{{ election.summary }} Results</h1>
+ <p>{{ election.description }}</p>
+ {% if election.url %}
+ <p><a href="{{ election.url }}">[More Information]</a></p>
+ {% endif %}
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates|sort(attribute='vote_count',reverse=True) %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{candidate.name}}
+ {% endif %}
+ {% if candidate.url %}
+ <small><a href="{{ candidate.url }}">[info]</a></small>
+ {% endif %}
+ </td>
+ <td>{{ candidate.vote_count }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+{% endblock %}
diff --git a/fedora_elections/templates/election/vote.html b/fedora_elections/templates/election/vote.html
index 5f18efd..e3c4dd4 100644
--- a/fedora_elections/templates/election/vote.html
+++ b/fedora_elections/templates/election/vote.html
@@ -6,22 +6,111 @@
<p><a href="{{election.url}}">[More Information]</a></p>
-<table>
- {% for candidate in election.candidates %}
- <tr>
- <td>{{candidate.name}} <a href="{{candidate.url}}">[Info]</a></td>
- <!-- If user is logged in: -->
- <td><!-- Render the wtform for the candidate voting box here --></td>
- <!-- End if user logged in -->
- </tr>
- {% endfor %}
-</table>
+<form action="" method="post">
+ <table border="1" cellpadding="1">
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>
+ {% if election.candidates_are_fasusers %}
+ {{ usernamemap[candidate.id] }}
+ {% else %}
+ {{ candidate.name }}
+ {% endif %}
+ <a href="{{candidate.url}}">[Info]</a>
+ </td>
+ <!-- If user is logged in: -->
+ {% if g.fas_user %}
+ {% if nextaction == 'vote' %}
+ <td>
+ <select name="{{candidate.id}}">
+ {% for opt in range(0, num_candidates+1) %}
+ <option value={{opt}}>{{opt}}</option>
+ {% endfor %}
+ </select>
+ </td>
+ {% endif %}
+ {% if nextaction == 'confirm' %}
+ <td>
+ {% set cid = candidate.id %}
+ <input type="hidden" name="{{candidate.id}}"
+ value="{{voteinfo[cid]}}" />
+ {% set weightpercent = (voteinfo[candidate.id]*100)/num_candidates %}
+ {% if weightpercent >= 90 %}
+ <div class="vthot">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 70 %}
+ <div class="vtwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 50 %}
+ <div class="vtmedwarm">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 30 %}
+ <div class="vtmedcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% elif weightpercent >= 10 %}
+ <div class="vtcool">
+ {{voteinfo[cid]}}
+ </div>
+ {% else %}
+ <div class="vtcold">
+ {{voteinfo[cid]}}
+ </div>
+ {% endif %}
+ </td>
+ {% endif %}
+ {% endif %}
+ <!-- End if user logged in -->
+ </tr>
+ {% endfor %}
+ </table>
+ {% if g.fas_user %}
+ <p>
+ {% if nextaction == 'confirm' %}
+ <input type="submit" name="{{nextaction}}" value="Submit" />
+ {% else %}
+ <input type="submit" name="{{nextaction}}" value="Preview" />
+ {% endif %}
+ </p>
+ {% endif %}
+</form>
<!-- Make this conditional on whether the user is logged in and can vote in the election -->
-To vote in this election you must be a member of any one of the following groups:
+{% if election.legal_voters.count() != 0 %}
+<p>
+ To vote in this election you must be a member of any one of the following groups:
+</p>
<ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
</ul>
+{% else %}
+<p />
+{% endif %}
+<div>
+ <h2>Information on Range Voting</h2>
+ <p>
+ Fedora Project has implemented
+ <a href="http://rangevoting.org/">Range Voting</a>
+ for this election, in particular the "Range (score-summing, blanks
+ treated as zero score, no quorum rule)" range voting system.
+ </p>
+ <p>
+ To cast your vote in this election simply select a value between 0 and
+ {{num_candidates}} with 0 as 'least or no preference' and
+ {{num_candidates}} as 'highest preference'.
+ </p>
+ <p>
+ At the end of the election, the highest ranking candidate(s) are marked
+ as the winners.
+ </p>
+ <p>
+ For more information about Range Voting, visit the
+ <a href="http://rangevoting.org/">Center for Range Voting</a>.
+ </p>
+</div>
{% endblock %}
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 90aafa1..2985fcc 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -1,26 +1,30 @@
{% extends "base.html" %}
{% block body %}
-<h2>Open elections</h2>
-<table>
- <tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- <th></th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>{{ election.summary }}</td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- <td>
- {% if election.status == "In progress" %}
- <a href="{{ url_for('vote', election_alias=election.alias) }}">Vote now!</a>
- {% endif %}
- </td>
- </tr>
- {% endfor %}
-</table>
+ <h2>{{ title }}</h2>
+ <table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{ election.summary }}</td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ <td>
+ {% if election.status == "In progress" %}
+ {% if g.fas_user %}
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">
+ Vote now!
+ </a>
+ {% endif %}
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
{% endblock %}
diff --git a/runserver.py b/runserver.py
index 176e2b9..b9103e0 100644
--- a/runserver.py
+++ b/runserver.py
@@ -1,2 +1,6 @@
+import __main__
+__main__.__requires__ = ['SQLAlchemy >= 0.7']
+import pkg_resources
+
from fedora_elections import app
-app.run()
+app.run(host='0.0.0.0', port=80)
commit b485bd715836ea03a380b539209e9083e17eae39
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 21:40:58 2012 -0400
Make fedmsg optional with a shim module.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index fb4fc87..78d2a16 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -2,7 +2,7 @@ import flask
from flask.ext.fas import FAS
from flask.ext.sqlalchemy import SQLAlchemy
-import fedmsg
+import fedmsgshim
from datetime import datetime, time
from functools import wraps
@@ -123,7 +123,7 @@ def admin_new_election():
db.session.add(admin)
db.session.commit()
flask.flash('Election "%s" added' % election.summary)
- fedmsg.publish(topic="election.new", msg=election)
+ fedmsgshim.publish(topic="election.new", msg=election)
return flask.redirect(flask.url_for('admin_view_election',
election_alias=election.alias))
return flask.render_template('admin/election_form.html', form=form,
diff --git a/fedora_elections/fedmsgshim.py b/fedora_elections/fedmsgshim.py
new file mode 100644
index 0000000..775f785
--- /dev/null
+++ b/fedora_elections/fedmsgshim.py
@@ -0,0 +1,17 @@
+""" This is a temporary shim that allows fedmsg to be an optional dependency of
+elections. If fedmsg is installed, these function calls will try to actually send
+messages. If it is not installed, it will return silently.
+
+ :Author: Ralph Bean <rbean(a)redhat.com>
+
+"""
+
+import warnings
+
+
+def publish(*args, **kwargs):
+ try:
+ import fedmsg
+ fedmsg.publish(*args, **kwargs)
+ except Exception, e:
+ warnings.warn(str(e))
commit b0557aa45becf446fc3e49a07904cad8e85f091d
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 21:28:21 2012 -0400
Revert "Add fedmsg to the list of requirements."
This reverts commit 471fabeecb4121b7837b7de509cee76385cfe8f8.
diff --git a/requirements.txt b/requirements.txt
index 36d8ca3..89a2349 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,4 +6,3 @@ python-fedora
Flask-FAS
Flask-sqlalchemy
Flask-wtf
-fedmsg>=0.3.9
commit 3eb4f4825a55d335b1b1c5d8eb3ee64eb1096e75
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 12:09:59 2012 -0400
Emit a fedmsg message when a new election is created.
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 2983de0..fb4fc87 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -2,6 +2,8 @@ import flask
from flask.ext.fas import FAS
from flask.ext.sqlalchemy import SQLAlchemy
+import fedmsg
+
from datetime import datetime, time
from functools import wraps
from urlparse import urlparse, urljoin
@@ -121,6 +123,7 @@ def admin_new_election():
db.session.add(admin)
db.session.commit()
flask.flash('Election "%s" added' % election.summary)
+ fedmsg.publish(topic="election.new", msg=election)
return flask.redirect(flask.url_for('admin_view_election',
election_alias=election.alias))
return flask.render_template('admin/election_form.html', form=form,
commit 5133c9a4e57c419e66d2665f1ad8760c4ab65c8f
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 12:09:38 2012 -0400
Add fedmsg to the list of requirements.
diff --git a/requirements.txt b/requirements.txt
index 89a2349..36d8ca3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,3 +6,4 @@ python-fedora
Flask-FAS
Flask-sqlalchemy
Flask-wtf
+fedmsg>=0.3.9
commit 2fcaaa54405acc0117f345b9a90022dc684d2649
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 12:09:20 2012 -0400
Add a fedmsg development config.
diff --git a/fedmsg.d/base.py b/fedmsg.d/base.py
new file mode 100644
index 0000000..dc1d155
--- /dev/null
+++ b/fedmsg.d/base.py
@@ -0,0 +1,39 @@
+# This file is part of fedmsg.
+# Copyright (C) 2012 Red Hat, Inc.
+#
+# fedmsg is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# fedmsg is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with fedmsg; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Ralph Bean <rbean(a)redhat.com>
+#
+config = dict(
+ # Set this to dev if you're hacking on fedmsg or an app.
+ # Set to stg or prod if running in the Fedora Infrastructure
+ environment="dev",
+
+ # Default is 0
+ high_water_mark=0,
+ io_threads=1,
+
+ ## For the fedmsg-hub and fedmsg-relay. ##
+
+ # We almost always want the fedmsg-hub to be sending messages with zmq as
+ # opposed to amqp or stomp.
+ zmq_enabled=True,
+
+ # When subscribing to messages, we want to allow splats ('*') so we tell the
+ # hub to not be strict when comparing messages topics to subscription
+ # topics.
+ zmq_strict=False,
+)
diff --git a/fedmsg.d/endpoints.py b/fedmsg.d/endpoints.py
new file mode 100644
index 0000000..88b37c0
--- /dev/null
+++ b/fedmsg.d/endpoints.py
@@ -0,0 +1,44 @@
+# This file is part of fedmsg.
+# Copyright (C) 2012 Red Hat, Inc.
+#
+# fedmsg is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# fedmsg is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with fedmsg; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Ralph Bean <rbean(a)redhat.com>
+#
+import socket
+hostname = socket.gethostname()
+
+config = dict(
+ # This is a dict of possible addresses from which fedmsg can send
+ # messages. fedmsg.init(...) requires that a 'name' argument be passed
+ # to it which corresponds with one of the keys in this dict.
+ endpoints={
+ # These are here so your local box can listen to the upstream
+ # infrastructure's bus. Cool, right? :)
+ "fedora-infrastructure": [
+ "tcp://fedoraproject.org:9940",
+ "tcp://stg.fedoraproject.org:9940",
+ ],
+
+ # This is the output side of the relay to which all other
+ # services can listen.
+ "relay_outbound": ["tcp://*:4001"],
+
+ # And this is what our app actually uses.
+ "fedora_elections.%s" % hostname: ["tcp://*:3007"],
+ },
+
+ relay_inbound="tcp://127.0.0.1:2003",
+)
diff --git a/fedmsg.d/ssl.py b/fedmsg.d/ssl.py
new file mode 100644
index 0000000..a286c77
--- /dev/null
+++ b/fedmsg.d/ssl.py
@@ -0,0 +1,41 @@
+# This file is part of fedmsg.
+# Copyright (C) 2012 Red Hat, Inc.
+#
+# fedmsg is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# fedmsg is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with fedmsg; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Ralph Bean <rbean(a)redhat.com>
+#
+import os
+import socket
+
+SEP = os.path.sep
+here = os.getcwd()
+hostname = socket.gethostname()
+
+config = dict(
+ sign_messages=False,
+ validate_signatures=False,
+ ssldir="/etc/pki/fedmsg",
+
+ crl_location="https://fedoraproject.org/fedmsg/crl.pem",
+ crl_cache="/tmp/crl.pem",
+ crl_cache_expiry=10,
+
+ certnames={
+ # In prod/stg, map hostname to the name of the cert in ssldir.
+ # Unfortunately, we can't use socket.getfqdn()
+ #"app01.stg": "app01.stg.phx2.fedoraproject.org",
+ },
+)
commit 29548ee3daaa3a374a8ee65d92fb657a097e8c1b
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 12:08:52 2012 -0400
Move kitchen above python-fedora in the requirements.txt list.
diff --git a/requirements.txt b/requirements.txt
index f6f22bc..89a2349 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,8 @@
# Used for when working from a virtualenv.
# Use this file by running "$ pip install -r requirements.txt"
flask
-python-fedora
kitchen
+python-fedora
Flask-FAS
Flask-sqlalchemy
Flask-wtf
commit 4709e0f153c93e4a74432e805dc7e4ae76a452c5
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Tue Aug 28 08:35:35 2012 -0700
Rest of the changes to merge about into vote
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 5b4cd80..2983de0 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -63,12 +63,6 @@ def archived_elections():
### ELECTION VIEWS #############################################
-(a)app.route('/about/<election_alias>')
-def about(election_alias):
- election = models.Election.query.filter_by(alias=election_alias).one()
- return flask.render_template('election/about.html', election=election)
-
-
@app.route('/vote/<election_alias>')
def vote(election_alias):
election = models.Election.query.filter_by(alias=election_alias).one()
diff --git a/fedora_elections/templates/election/vote.html b/fedora_elections/templates/election/vote.html
index f1e6a55..5f18efd 100644
--- a/fedora_elections/templates/election/vote.html
+++ b/fedora_elections/templates/election/vote.html
@@ -10,10 +10,14 @@
{% for candidate in election.candidates %}
<tr>
<td>{{candidate.name}} <a href="{{candidate.url}}">[Info]</a></td>
+ <!-- If user is logged in: -->
+ <td><!-- Render the wtform for the candidate voting box here --></td>
+ <!-- End if user logged in -->
</tr>
{% endfor %}
</table>
+<!-- Make this conditional on whether the user is logged in and can vote in the election -->
To vote in this election you must be a member of any one of the following groups:
<ul>
{% for group in election.legal_voters %}
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 8dc8fb4..90aafa1 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -17,7 +17,7 @@
<td>{{ election.status }}</td>
<td>
{% if election.status == "In progress" %}
- <a href="{{ url_for('about', election_alias=election.alias) }}">Vote now!</a>
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">Vote now!</a>
{% endif %}
</td>
</tr>
diff --git a/tests/base.py b/tests/base.py
index 2edf911..e12179b 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -22,7 +22,7 @@ class BaseRequestTests(object):
fedora_elections.db.drop_all()
url_subs = {'election_alias': None, 'candidate_id': None}
-anon_urls = ('/', '/archive', '/login', '/logout', '/about/%(election_alias)s')
+anon_urls = ('/', '/archive', '/login', '/logout', '/vote/%(election_alias)s')
authed_view_urls = ('/logout', '/vote/%(election_alias)s')
admin_view_urls = ('/admin', '/admin/new', '/admin/%(election_alias)s')
admin_modify_urls = ('/admin/new', '/admin/%(election_alias)s/edit',
commit 21951c7d86c0c691c1c7b645edc08e164d176805
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Tue Aug 28 08:34:47 2012 -0700
Merge the about page into the vote page
diff --git a/fedora_elections/templates/election/about.html b/fedora_elections/templates/election/about.html
deleted file mode 100644
index f1e6a55..0000000
--- a/fedora_elections/templates/election/about.html
+++ /dev/null
@@ -1,23 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-<h2>{{election.summary}}</h2>
-
-<p>{{election.description}}</p>
-
-<p><a href="{{election.url}}">[More Information]</a></p>
-
-<table>
- {% for candidate in election.candidates %}
- <tr>
- <td>{{candidate.name}} <a href="{{candidate.url}}">[Info]</a></td>
- </tr>
- {% endfor %}
-</table>
-
-To vote in this election you must be a member of any one of the following groups:
-<ul>
- {% for group in election.legal_voters %}
- <li>{{group.group_name}}</li>
- {% endfor %}
-</ul>
-{% endblock %}
diff --git a/fedora_elections/templates/election/vote.html b/fedora_elections/templates/election/vote.html
index 90aafa1..f1e6a55 100644
--- a/fedora_elections/templates/election/vote.html
+++ b/fedora_elections/templates/election/vote.html
@@ -1,26 +1,23 @@
{% extends "base.html" %}
{% block body %}
-<h2>Open elections</h2>
+<h2>{{election.summary}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
<table>
+ {% for candidate in election.candidates %}
<tr>
- <th>Election</th>
- <th>Start date (UTC)</th>
- <th>End date (UTC)</th>
- <th>Status</th>
- <th></th>
- </tr>
- {% for election in elections %}
- <tr>
- <td>{{ election.summary }}</td>
- <td>{{ election.start_date }}</td>
- <td>{{ election.end_date }}</td>
- <td>{{ election.status }}</td>
- <td>
- {% if election.status == "In progress" %}
- <a href="{{ url_for('vote', election_alias=election.alias) }}">Vote now!</a>
- {% endif %}
- </td>
+ <td>{{candidate.name}} <a href="{{candidate.url}}">[Info]</a></td>
</tr>
{% endfor %}
</table>
+
+To vote in this election you must be a member of any one of the following groups:
+<ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+</ul>
{% endblock %}
commit a34d75e1e9535e5d4bd4cd7685237e7888736dba
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 18:47:41 2012 -0700
Some more pages and functions that are blockers
diff --git a/TODO b/TODO
index 10e8e54..9858c8f 100644
--- a/TODO
+++ b/TODO
@@ -1,6 +1,9 @@
Things that need doing to get this deployed
===========================================
+* Add legalvoters to an election
+* Add page to vote
+* Add page that summarizes the vote results
* Write a conversion script for the old db model to the new one
* rpmify
* rpmify flask-fas (or get flask-fas into python-fedora)
commit 75db248833c113f3953b5dbb453386a3400f6b88
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 18:44:59 2012 -0700
Add an implementation of a page about an election
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index d59fa64..5b4cd80 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -61,6 +61,20 @@ def archived_elections():
raise NotImplementedError
+### ELECTION VIEWS #############################################
+
+(a)app.route('/about/<election_alias>')
+def about(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ return flask.render_template('election/about.html', election=election)
+
+
+(a)app.route('/vote/<election_alias>')
+def vote(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).one()
+ return flask.render_template('election/vote.html', election=election)
+
+
### AUTH VIEWS #############################################
@app.route('/login', methods=('GET', 'POST'))
diff --git a/fedora_elections/templates/election/about.html b/fedora_elections/templates/election/about.html
new file mode 100644
index 0000000..f1e6a55
--- /dev/null
+++ b/fedora_elections/templates/election/about.html
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{election.summary}}</h2>
+
+<p>{{election.description}}</p>
+
+<p><a href="{{election.url}}">[More Information]</a></p>
+
+<table>
+ {% for candidate in election.candidates %}
+ <tr>
+ <td>{{candidate.name}} <a href="{{candidate.url}}">[Info]</a></td>
+ </tr>
+ {% endfor %}
+</table>
+
+To vote in this election you must be a member of any one of the following groups:
+<ul>
+ {% for group in election.legal_voters %}
+ <li>{{group.group_name}}</li>
+ {% endfor %}
+</ul>
+{% endblock %}
diff --git a/fedora_elections/templates/election/vote.html b/fedora_elections/templates/election/vote.html
new file mode 100644
index 0000000..90aafa1
--- /dev/null
+++ b/fedora_elections/templates/election/vote.html
@@ -0,0 +1,26 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>Open elections</h2>
+<table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{ election.summary }}</td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ <td>
+ {% if election.status == "In progress" %}
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">Vote now!</a>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+</table>
+{% endblock %}
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 90aafa1..8dc8fb4 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -17,7 +17,7 @@
<td>{{ election.status }}</td>
<td>
{% if election.status == "In progress" %}
- <a href="{{ url_for('vote', election_alias=election.alias) }}">Vote now!</a>
+ <a href="{{ url_for('about', election_alias=election.alias) }}">Vote now!</a>
{% endif %}
</td>
</tr>
commit 48c4d127e65bf00c38a28052ccba67aeefb7c1dd
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 18:42:28 2012 -0700
Add two new pages for dsmoketesting
diff --git a/tests/base.py b/tests/base.py
index 0223c82..2edf911 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -22,8 +22,8 @@ class BaseRequestTests(object):
fedora_elections.db.drop_all()
url_subs = {'election_alias': None, 'candidate_id': None}
-anon_urls = ('/', '/archive', '/login', '/logout')
-authed_view_urls = ('/logout',)
+anon_urls = ('/', '/archive', '/login', '/logout', '/about/%(election_alias)s')
+authed_view_urls = ('/logout', '/vote/%(election_alias)s')
admin_view_urls = ('/admin', '/admin/new', '/admin/%(election_alias)s')
admin_modify_urls = ('/admin/new', '/admin/%(election_alias)s/edit',
'/admin/%(election_alias)s/candidates/new',
commit 671cf5562f4ad90c7d9eea652e4e7aa2d3d67db9
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 18:23:22 2012 -0700
A few more todo items: i18n, more paranoid error handling
diff --git a/TODO b/TODO
index 335107f..10e8e54 100644
--- a/TODO
+++ b/TODO
@@ -10,6 +10,8 @@ Things that need doing to get this deployed
us to make sure that no data changing requests are coded without wtf forms.
So this is really: audit to make sure everything is using wtf forms
+* Error handling -- unknown elections, etc
+
Future enhancements (anything not necessary for deploying)
==========================================================
@@ -26,3 +28,7 @@ Future enhancements (anything not necessary for deploying)
should go in a different module
*** return data to templating engine
*** Association with a URL
+* i18n. Need review: https://bugzilla.redhat.com/show_bug.cgi?id=839071
+** mark strings
+** Pull translatable strings out
+** set up transifex
commit d05250abdd86aec4534e8e839d487d719b17c33f
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 17:43:22 2012 -0700
Smoketesting now covers all urls' GET method.
diff --git a/tests/base.py b/tests/base.py
index 8271e65..0223c82 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -21,10 +21,11 @@ class BaseRequestTests(object):
fedora_elections.db.session.remove()
fedora_elections.db.drop_all()
+url_subs = {'election_alias': None, 'candidate_id': None}
anon_urls = ('/', '/archive', '/login', '/logout')
-admin_urls = ('/admin', '/admin/new', '/admin/%(election_alias)s',
- '/admin/%(election_alias)s/edit',
+authed_view_urls = ('/logout',)
+admin_view_urls = ('/admin', '/admin/new', '/admin/%(election_alias)s')
+admin_modify_urls = ('/admin/new', '/admin/%(election_alias)s/edit',
'/admin/%(election_alias)s/candidates/new',
'/admin/%(election_alias)s/candidates/%(candidate_id)d/edit',
'/admin/%(election_alias)s/candidates/%(candidate_id)d/delete')
-authed_urls = ('/logout',)
diff --git a/tests/test_smoke.py b/tests/test_smoke.py
index 6b03e8e..1d7b306 100644
--- a/tests/test_smoke.py
+++ b/tests/test_smoke.py
@@ -2,19 +2,42 @@
#
# Copyright (C) 2012 Red Hat, Inc.
#
-# Author(s): Toshio Kuratomi <toshio(a)fedoraproject.org>
+# Author(s): Toshio Kuratomi <toshio(a)fedoraproject.org>
+from copy import copy
+import datetime
from functools import partial
+from mock import Mock
from nose import tools, with_setup
-from base import BaseRequestTests, anon_urls, authed_urls, admin_urls
+from base import BaseRequestTests, anon_urls, authed_view_urls, \
+ admin_view_urls, admin_modify_urls, url_subs
+
+from fedora_elections import db, models
+from fedora_elections import fas
class TestSmoke(BaseRequestTests):
'''Basic test that we don't get 500s'''
def setUp(self):
super(TestSmoke, self).setUp()
+ fas.login = Mock(return_value=True)
+ # Add an election
+ election = models.Election(summary='Test Election 1',
+ alias='test1', description='lorem ipsum of course',
+ url='http://fedorahosted.org/elections',
+ start_date=datetime.datetime.utcnow(),
+ end_date=datetime.datetime.utcnow()+datetime.timedelta(hours=1))
+ db.session.add(election)
+ # Add candidate to election
+ candidate = models.Candidate(election=election, name='Mr Testy', url='http://fedoraproject.org')
+ db.session.add(candidate)
+ db.session.commit()
+
+ self.url_subs = copy(url_subs)
+ self.url_subs['election_alias'] = 'test1'
+ self.url_subs['candidate_id'] = candidate.id
def tearDown(self):
super(TestSmoke, self).tearDown()
@@ -29,7 +52,7 @@ class TestSmoke(BaseRequestTests):
return self.app.get('/logout', follow_redirects=True)
def check_status_code(self, url, status_codes=[200]):
- resp = self.app.get(url, follow_redirects=True)
+ resp = self.app.get(url % self.url_subs, follow_redirects=True)
tools.assert_in(resp.status_code, status_codes)
@with_setup(partial(setup_auth, 'user', '123'), teardown_auth)
@@ -40,15 +63,24 @@ class TestSmoke(BaseRequestTests):
def check_admin_status_code(self, *args, **kwargs):
self.check_status_code(*args, **kwargs)
- def test_anon(self):
+ def test_all_url_subs(self):
+ '''Testing that the testsuite is going to substitute all variables in the url.'''
+ for i in self.url_subs.values():
+ if i is None:
+ raise Exception('Smoke test must be updated for new url variable present in base.py')
+
+ def test_anon_view(self):
for url in anon_urls:
yield self.check_status_code, url, [200]
- def test_authed(self):
- for url in (u for u in authed_urls):
+ def test_authed_view(self):
+ for url in (u for u in authed_view_urls):
yield self.check_user_status_code, url, [200]
- def test_admin(self):
- self.setup_auth('admin', '123')
- for url in (u for u in admin_urls):
+ def test_admin_view(self):
+ for url in (u for u in admin_view_urls):
+ yield self.check_admin_status_code, url, [200]
+
+ def test_admin_modify(self):
+ for url in (u for u in admin_modify_urls):
yield self.check_admin_status_code, url, [200]
commit 4b3fcea5e4868054f8f8da80239a6e66e1183551
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 15:28:54 2012 -0700
Add ability to specify site-specifc css
diff --git a/development.cfg b/development.cfg
index 5993209..6e045f2 100644
--- a/development.cfg
+++ b/development.cfg
@@ -10,3 +10,5 @@ FAS_CHECK_CERT = False
FAS_HTTPS_REQUIRED = False
FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
+# List of URLs for additional site-wide CSS
+EXTRA_CSS = []
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
index 75f2543..b167ba4 100644
--- a/fedora_elections/templates/base.html
+++ b/fedora_elections/templates/base.html
@@ -4,6 +4,8 @@
{% block head %}
<title>Fedora Elections{% if self.title() %} — {% block title %}{% endblock %}{% endif %}</title>
<link rel="stylesheet" href="{{ url_for("static", filename="style.css") }}">
+ {% for css in config.get('EXTRA_CSS', []) %}<link rel="stylesheet" href="{{ css }}">
+ {% endfor %}
{% endblock %}
</head>
<body>
commit 36ded228f98e02af3da6ac14c49e2d2027734de7
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 15:18:49 2012 -0700
Fix traceback for hitting logout when the user is not logged in
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 9570055..d59fa64 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -80,7 +80,7 @@ def auth_login():
@app.route('/logout')
def auth_logout():
if not flask.g.fas_user:
- return safe_redirect_back()
+ return redirect.safe_redirect_back()
fas.logout()
flask.flash('You have been logged out')
return redirect.safe_redirect_back()
commit 7e5dec7001c0b6faa6721ecc235760cf25029158
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 15:18:14 2012 -0700
Add unittests -- currently just a few smoketests
diff --git a/testing b/testing
new file mode 100755
index 0000000..529e8ee
--- /dev/null
+++ b/testing
@@ -0,0 +1,2 @@
+#!/bin/sh
+FEDORA_ELECTIONS_CONFIG=../development.cfg nosetests $*
diff --git a/tests/base.py b/tests/base.py
new file mode 100644
index 0000000..8271e65
--- /dev/null
+++ b/tests/base.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2012 Red Hat, Inc.
+#
+# Author(s): Toshio Kuratomi <toshio(a)fedoraproject.org>
+
+import fedora_elections
+
+# Note: This is used in unittests that use nose's generator tests so we can't
+# subclass unittest.TestCase
+class BaseRequestTests(object):
+ '''Setup application for further testing of requests'''
+
+ def setUp(self):
+ fedora_elections.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + '../tests/testing.db'
+ fedora_elections.app.config['TESTING'] = True
+ self.app = fedora_elections.app.test_client()
+ fedora_elections.db.create_all()
+
+ def tearDown(self):
+ fedora_elections.db.session.remove()
+ fedora_elections.db.drop_all()
+
+anon_urls = ('/', '/archive', '/login', '/logout')
+admin_urls = ('/admin', '/admin/new', '/admin/%(election_alias)s',
+ '/admin/%(election_alias)s/edit',
+ '/admin/%(election_alias)s/candidates/new',
+ '/admin/%(election_alias)s/candidates/%(candidate_id)d/edit',
+ '/admin/%(election_alias)s/candidates/%(candidate_id)d/delete')
+authed_urls = ('/logout',)
diff --git a/tests/test_smoke.py b/tests/test_smoke.py
new file mode 100644
index 0000000..6b03e8e
--- /dev/null
+++ b/tests/test_smoke.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2012 Red Hat, Inc.
+#
+# Author(s): Toshio Kuratomi <toshio(a)fedoraproject.org>
+
+from functools import partial
+
+from nose import tools, with_setup
+
+from base import BaseRequestTests, anon_urls, authed_urls, admin_urls
+
+class TestSmoke(BaseRequestTests):
+ '''Basic test that we don't get 500s'''
+
+ def setUp(self):
+ super(TestSmoke, self).setUp()
+
+ def tearDown(self):
+ super(TestSmoke, self).tearDown()
+
+ def setup_auth(self, username, password):
+ self.setUp()
+ self.app.post('/login', data=dict( username=username,
+ password=password), follow_redirects=True)
+
+ def teardown_auth(self):
+ self.tearDown()
+ return self.app.get('/logout', follow_redirects=True)
+
+ def check_status_code(self, url, status_codes=[200]):
+ resp = self.app.get(url, follow_redirects=True)
+ tools.assert_in(resp.status_code, status_codes)
+
+ @with_setup(partial(setup_auth, 'user', '123'), teardown_auth)
+ def check_user_status_code(self, *args, **kwargs):
+ self.check_status_code(*args, **kwargs)
+
+ @with_setup(partial(setup_auth, 'admin', '123'), teardown_auth)
+ def check_admin_status_code(self, *args, **kwargs):
+ self.check_status_code(*args, **kwargs)
+
+ def test_anon(self):
+ for url in anon_urls:
+ yield self.check_status_code, url, [200]
+
+ def test_authed(self):
+ for url in (u for u in authed_urls):
+ yield self.check_user_status_code, url, [200]
+
+ def test_admin(self):
+ self.setup_auth('admin', '123')
+ for url in (u for u in admin_urls):
+ yield self.check_admin_status_code, url, [200]
commit e8098950006e1e3ae0d40638d412f0a182003703
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 11:27:11 2012 -0700
Add a TODO file
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..335107f
--- /dev/null
+++ b/TODO
@@ -0,0 +1,28 @@
+Things that need doing to get this deployed
+===========================================
+
+* Write a conversion script for the old db model to the new one
+* rpmify
+* rpmify flask-fas (or get flask-fas into python-fedora)
+* Implement a voting method for range voting
+
+* CSRF protection -- anything going through wtf forms is protected. It's up to
+ us to make sure that no data changing requests are coded without wtf forms.
+ So this is really: audit to make sure everything is using wtf forms
+
+Future enhancements (anything not necessary for deploying)
+==========================================================
+
+* Make voting methods modular
+* Give this a Fedora theme
+* Break the main program logic out into library modules
+** Controllers should be as lightweight as possible
+*** validation of input
+**** Might decorate the controller method but the validation itself should live
+ in another module. Might want to break validation schemas into another
+ module too
+*** Authentication
+**** decorators can go here and calls that return True or False. Logic itself
+ should go in a different module
+*** return data to templating engine
+*** Association with a URL
commit 8a345a12afebbbf1f5ed2e786d6155946e494a06
Author: Ralph Bean <rbean(a)redhat.com>
Date: Mon Aug 20 15:20:22 2012 -0400
Added preliminary requirements.txt.
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..f6f22bc
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,8 @@
+# Used for when working from a virtualenv.
+# Use this file by running "$ pip install -r requirements.txt"
+flask
+python-fedora
+kitchen
+Flask-FAS
+Flask-sqlalchemy
+Flask-wtf
commit 6d36e42e9d1b81d5c4e924c0282a303a8bd36ef7
Author: Ian Weller <ian(a)ianweller.org>
Date: Sun Jul 15 09:54:58 2012 -0500
List frontpaged elections on front page
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
index 00269bc..9570055 100644
--- a/fedora_elections/__init__.py
+++ b/fedora_elections/__init__.py
@@ -53,7 +53,7 @@ def election_admin_required(f):
@app.route('/')
def index():
elections = models.Election.query.filter_by(frontpage=True).all()
- return flask.render_template('list/index.html')
+ return flask.render_template('list/index.html', elections=elections)
@app.route('/archive')
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
index b07108b..e40f882 100644
--- a/fedora_elections/templates/admin/all_elections.html
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -1,6 +1,7 @@
{% extends "base.html" %}
+{% block title %}Election administration{% endblock %}
{% block body %}
-<h2>My elections</h2>
+<h2>Election administration</h2>
<table>
<tr>
<th>Election</th>
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
index 1ad207a..90aafa1 100644
--- a/fedora_elections/templates/list/index.html
+++ b/fedora_elections/templates/list/index.html
@@ -1,4 +1,26 @@
{% extends "base.html" %}
{% block body %}
-<p>Hello, world!</p>
+<h2>Open elections</h2>
+<table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td>{{ election.summary }}</td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ <td>
+ {% if election.status == "In progress" %}
+ <a href="{{ url_for('vote', election_alias=election.alias) }}">Vote now!</a>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+</table>
{% endblock %}
commit e21903e83b6f352324f05dcdaeaa01b3133ec8de
Author: Ian Weller <ian(a)ianweller.org>
Date: Thu Jul 12 16:59:14 2012 -0500
Initial commit with mostly-working admin
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bb585f9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+database.sqlite
+*.pyc
+*.pyo
+*.swp
diff --git a/createdb b/createdb
new file mode 100755
index 0000000..4699a56
--- /dev/null
+++ b/createdb
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_CONFIG=../development.cfg python createdb.py
diff --git a/createdb.py b/createdb.py
new file mode 100644
index 0000000..e99a35c
--- /dev/null
+++ b/createdb.py
@@ -0,0 +1,2 @@
+from fedora_elections import db
+db.create_all()
diff --git a/development.cfg b/development.cfg
new file mode 100644
index 0000000..5993209
--- /dev/null
+++ b/development.cfg
@@ -0,0 +1,12 @@
+import os, os.path
+
+DEBUG = True
+# You will want to change this for your install
+SECRET_KEY = 'change me'
+SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.getcwd(), 'database.sqlite')
+
+FAS_BASE_URL = 'https://fakefas.fedoraproject.org/accounts/'
+FAS_CHECK_CERT = False
+FAS_HTTPS_REQUIRED = False
+
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
diff --git a/fedora_elections/__init__.py b/fedora_elections/__init__.py
new file mode 100644
index 0000000..00269bc
--- /dev/null
+++ b/fedora_elections/__init__.py
@@ -0,0 +1,207 @@
+import flask
+from flask.ext.fas import FAS
+from flask.ext.sqlalchemy import SQLAlchemy
+
+from datetime import datetime, time
+from functools import wraps
+from urlparse import urlparse, urljoin
+
+app = flask.Flask(__name__)
+app.config.from_object('fedora_elections.default_config')
+app.config.from_envvar('FEDORA_ELECTIONS_CONFIG')
+
+# set up SQLAlchemy
+db = SQLAlchemy(app)
+
+# set up FAS
+fas = FAS(app)
+
+
+# modular imports
+from fedora_elections import forms, models, redirect
+
+
+def remove_csrf(form_data):
+ return dict([(k, v) for k, v in form_data.items() if k != 'csrf'])
+
+
+def login_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if flask.g.fas_user is None:
+ return flask.redirect(flask.url_for('auth_login',
+ next=flask.request.url))
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+def election_admin_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if flask.g.fas_user is None:
+ return flask.redirect(flask.url_for('auth_login',
+ next=flask.request.url))
+ if app.config['FEDORA_ELECTIONS_ADMIN_GROUP'] not in \
+ flask.g.fas_user.groups:
+ flask.abort(403)
+ return f(*args, **kwargs)
+ return decorated_function
+
+
+### LIST VIEWS #############################################
+
+(a)app.route('/')
+def index():
+ elections = models.Election.query.filter_by(frontpage=True).all()
+ return flask.render_template('list/index.html')
+
+
+(a)app.route('/archive')
+def archived_elections():
+ raise NotImplementedError
+
+
+### AUTH VIEWS #############################################
+
+(a)app.route('/login', methods=('GET', 'POST'))
+def auth_login():
+ if flask.g.fas_user:
+ return redirect.safe_redirect_back()
+ form = forms.LoginForm()
+ if form.validate_on_submit():
+ if fas.login(form.username.data, form.password.data):
+ flask.flash('Welcome, %s' % flask.g.fas_user.username)
+ return redirect.safe_redirect_back()
+ else:
+ flask.flash('Incorrect username or password')
+ return flask.render_template('auth/login.html', form=form)
+
+
+(a)app.route('/logout')
+def auth_logout():
+ if not flask.g.fas_user:
+ return safe_redirect_back()
+ fas.logout()
+ flask.flash('You have been logged out')
+ return redirect.safe_redirect_back()
+
+
+### ELECTION ADMIN VIEWS ###################################
+
+(a)app.route('/admin/')
+@election_admin_required
+def admin_view_elections():
+ elections = models.Election.query.filter_by().all()
+ return flask.render_template('admin/all_elections.html',
+ elections=elections)
+
+
+(a)app.route('/admin/new', methods=("GET", "POST"))
+@election_admin_required
+def admin_new_election():
+ form = forms.ElectionForm()
+ if form.validate_on_submit():
+ election = models.Election(**remove_csrf(form.data))
+ # Fix start_date and end_date to use datetime
+ election.start_date = datetime.combine(election.start_date, time())
+ election.end_date = datetime.combine(election.end_date,
+ time(23, 59, 59))
+ db.session.add(election)
+ admin = models.ElectionAdminGroup(election=election,
+ group_name=app.config['FEDORA_ELECTIONS_ADMIN_GROUP'],
+ role_required=u'user')
+ db.session.add(admin)
+ db.session.commit()
+ flask.flash('Election "%s" added' % election.summary)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/election_form.html', form=form,
+ submit_text='Create election')
+
+
+(a)app.route('/admin/<election_alias>/')
+@election_admin_required
+def admin_view_election(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ return flask.render_template('admin/view_election.html', election=election)
+
+
+(a)app.route('/admin/<election_alias>/edit', methods=("GET", "POST"))
+@election_admin_required
+def admin_edit_election(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ form = forms.ElectionForm(election.id, obj=election)
+ if form.validate_on_submit():
+ form.populate_obj(election)
+ db.session.commit()
+ flask.flash('Election "%s" saved' % election.summary)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/election_form.html', form=form,
+ submit_text='Edit election')
+
+
+(a)app.route('/admin/<election_alias>/candidates/new', methods=("GET", "POST"))
+@election_admin_required
+def admin_add_candidate(election_alias):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ if not election:
+ flask.abort(404)
+ form = forms.CandidateForm()
+ if form.validate_on_submit():
+ candidate = models.Candidate(election=election,
+ **remove_csrf(form.data))
+ db.session.add(candidate)
+ db.session.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/candidate.html', form=form,
+ submit_text='Add candidate')
+
+
+(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
+ methods=("GET", "POST"))
+@election_admin_required
+def admin_edit_candidate(election_alias, candidate_id):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ if not election or not candidate:
+ flask.abort(404)
+ if candidate.election != election:
+ flask.abort(404)
+ form = forms.CandidateForm(obj=candidate)
+ if form.validate_on_submit():
+ form.populate_obj(candidate)
+ db.session.commit()
+ flask.flash('Candidate "%s" saved' % candidate.name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/candidate.html', form=form,
+ submit_text='Edit candidate')
+
+
+(a)app.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
+ methods=("GET", "POST"))
+@election_admin_required
+def admin_delete_candidate(election_alias, candidate_id):
+ election = models.Election.query.filter_by(alias=election_alias).first()
+ candidate = models.Candidate.query.filter_by(id=candidate_id).first()
+ if not election or not candidate:
+ flask.abort(404)
+ if candidate.election != election:
+ flask.abort(404)
+ form = forms.ConfirmationForm()
+ if form.validate_on_submit():
+ db.session.delete(candidate)
+ candidate_name = candidate.name
+ db.session.commit()
+ flask.flash('Candidate "%s" deleted' % candidate_name)
+ return flask.redirect(flask.url_for('admin_view_election',
+ election_alias=election.alias))
+ return flask.render_template('admin/delete_candidate.html', form=form,
+ candidate=candidate)
diff --git a/fedora_elections/default_config.py b/fedora_elections/default_config.py
new file mode 100644
index 0000000..7b5b794
--- /dev/null
+++ b/fedora_elections/default_config.py
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_ADMIN_GROUP = 'elections'
diff --git a/fedora_elections/forms.py b/fedora_elections/forms.py
new file mode 100644
index 0000000..f050dbd
--- /dev/null
+++ b/fedora_elections/forms.py
@@ -0,0 +1,69 @@
+from flask.ext import wtf
+
+from fedora_elections.models import Election
+
+
+class ElectionForm(wtf.Form):
+ summary = wtf.TextField('Summary', [wtf.validators.Required(),
+ wtf.validators.Length(max=150)])
+ alias = wtf.TextField('Alias', [
+ wtf.validators.Required(),
+ wtf.validators.Length(max=100),
+ wtf.validators.Regexp('[a-z0-9_-]+',
+ message=('Alias may only contain lower case '
+ 'letters, numbers, hyphens and '
+ 'underscores.')),
+ ])
+ description = wtf.TextAreaField('Description', [wtf.validators.Required()])
+ url = wtf.TextField('URL', [wtf.validators.Required(),
+ wtf.validators.URL(),
+ wtf.validators.Length(max=250)])
+ start_date = wtf.DateField('Start date', [wtf.validators.Required()])
+ end_date = wtf.DateField('End date', [wtf.validators.Required()])
+ number_elected = wtf.IntegerField('Number elected',
+ [wtf.validators.Required(),
+ wtf.validators.NumberRange(min=1)],
+ default=1)
+ candidates_are_fasusers = wtf.BooleanField('Candidates are FAS users?')
+ frontpage = wtf.BooleanField('Show on front page?')
+ embargoed = wtf.BooleanField('Embargo results?', default=True)
+
+ def __init__(form, election_id=None, *args, **kwargs):
+ super(ElectionForm, form).__init__(*args, **kwargs)
+ form._election_id = election_id
+
+ def validate_summary(form, field):
+ check = Election.query.filter_by(summary=form.summary.data).all()
+ if check:
+ if not (form._election_id and form._election_id == check[0].id):
+ raise wtf.ValidationError('There is already another election '
+ 'with this summary.')
+
+ def validate_alias(form, field):
+ if form.alias.data == 'new':
+ raise wtf.ValidationError(flask.Markup('The alias cannot be '
+ '<code>new</code>.'))
+ check = Election.query.filter_by(alias=form.alias.data).all()
+ if check:
+ if not (form._election_id and form._election_id == check[0].id):
+ raise wtf.ValidationError('There is already another election '
+ 'with this alias.')
+
+ def validate_end_date(form, field):
+ if form.end_date.data <= form.start_date.data:
+ raise wtf.ValidationError('End date must be later than '
+ 'start date.')
+
+
+class CandidateForm(wtf.Form):
+ name = wtf.TextField('Name', [wtf.validators.Required()])
+ url = wtf.TextField('URL', [wtf.validators.Length(max=250)])
+
+
+class LoginForm(wtf.Form):
+ username = wtf.TextField('Username', [wtf.validators.Required()])
+ password = wtf.PasswordField('Password', [wtf.validators.Required()])
+
+
+class ConfirmationForm(wtf.Form):
+ pass
diff --git a/fedora_elections/models.py b/fedora_elections/models.py
new file mode 100644
index 0000000..f025255
--- /dev/null
+++ b/fedora_elections/models.py
@@ -0,0 +1,94 @@
+from fedora_elections import app, db
+
+from datetime import datetime
+
+
+class Election(db.Model):
+ __tablename__ = 'elections'
+
+ id = db.Column(db.Integer, primary_key=True)
+ summary = db.Column(db.Unicode(150), unique=True, nullable=False)
+ alias = db.Column(db.Unicode(100), unique=True, nullable=False)
+ description = db.Column(db.UnicodeText, nullable=False)
+ url = db.Column(db.Unicode(250), nullable=False)
+ start_date = db.Column(db.DateTime, nullable=False)
+ end_date = db.Column(db.DateTime, nullable=False)
+ number_elected = db.Column(db.Integer, nullable=False, default=1)
+ embargoed = db.Column(db.Boolean, nullable=False, default=True)
+ frontpage = db.Column(db.Boolean, nullable=False, default=False)
+ voting_type = db.Column(db.Unicode(100), nullable=False, default=u'range')
+ candidates_are_fasusers = db.Column(db.Boolean, nullable=False,
+ default=False)
+
+ @property
+ def status(self):
+ if datetime.now() < self.start_date:
+ return 'Pending'
+ else:
+ if datetime.now() < self.end_date:
+ return 'In progress'
+ else:
+ if self.embargoed:
+ return 'Embargoed'
+ else:
+ return 'Ended'
+
+ @property
+ def locked(self):
+ return datetime.now() >= self.start_date
+
+
+class ElectionAdminGroup(db.Model):
+ __tablename__ = 'electionadmingroups'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('admin_groups',
+ lazy='dynamic'))
+ group_name = db.Column(db.Unicode(150), nullable=False)
+ role_required = db.Column(db.Enum(u'user', u'sponsor', u'administrator'),
+ nullable=False)
+
+
+class Candidate(db.Model):
+ __tablename__ = 'candidates'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('candidates',
+ lazy='dynamic'))
+ name = db.Column(db.Unicode(150), nullable=False) # FAS username if
+ # candidates_are_fasusers
+ url = db.Column(db.Unicode(250))
+
+
+class LegalVoter(db.Model):
+ __tablename__ = 'legalvoters'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('legal_voters',
+ lazy='dynamic'))
+ # special names:
+ # "cla + one" = cla_done + 1 non-cla group
+ group_name = db.Column(db.Unicode(150), nullable=False)
+
+
+class Vote(db.Model):
+ __tablename__ = 'votes'
+
+ id = db.Column(db.Integer, primary_key=True)
+ election_id = db.Column(db.Integer, db.ForeignKey('elections.id'),
+ nullable=False)
+ election = db.relationship('Election', backref=db.backref('votes',
+ lazy='dynamic'))
+ voter = db.Column(db.Unicode(150), nullable=False)
+ timestamp = db.Column(db.DateTime, nullable=False)
+ candidate_id = db.Column(db.Integer, db.ForeignKey('candidates.id'),
+ nullable=False)
+ candidate = db.relationship('Candidate', backref=db.backref('votes',
+ lazy='dynamic'))
+ value = db.Column(db.Integer, nullable=False)
diff --git a/fedora_elections/redirect.py b/fedora_elections/redirect.py
new file mode 100644
index 0000000..5571abe
--- /dev/null
+++ b/fedora_elections/redirect.py
@@ -0,0 +1,23 @@
+import flask
+
+from urlparse import urljoin, urlparse
+
+
+def is_safe_url(target):
+ ref_url = urlparse(flask.request.host_url)
+ test_url = urlparse(urljoin(flask.request.host_url, target))
+ return test_url.scheme in ('http', 'https') and \
+ ref_url.netloc == test_url.netloc
+
+
+def safe_redirect_back(next=None, fallback=('index', {})):
+ targets = []
+ if next:
+ targets.append(next)
+ if 'next' in flask.request.args and \
+ flask.request.args['next']:
+ targets.append(flask.request.args['next'])
+ targets.append(flask.url_for(fallback[0], **fallback[1]))
+ for target in targets:
+ if is_safe_url(target):
+ return flask.redirect(target)
diff --git a/fedora_elections/static/style.css b/fedora_elections/static/style.css
new file mode 100644
index 0000000..b316ea7
--- /dev/null
+++ b/fedora_elections/static/style.css
@@ -0,0 +1,90 @@
+body {
+ font-family: sans-serif;
+ font-size: 12pt;
+ line-height: 1.35;
+}
+
+#flashes {
+ background-color: #ffa;
+}
+
+nav {
+ background-color: #eee;
+ padding: 0.5em 1em;
+}
+
+nav h2 {
+ display: inline;
+ font-size: inherit;
+ margin-right: 1em;
+}
+
+nav ul {
+ display: inline;
+ list-style: none;
+ padding: 0;
+}
+
+nav li {
+ display: inline;
+ margin-right: .5em;
+}
+
+.clear {
+ clear: both;
+}
+
+/* input elements */
+
+fieldset {
+ border: 0;
+ padding: 0;
+ margin: .5em 0;
+ float: left;
+ clear: both;
+}
+
+.row {
+ margin: .25em 0;
+}
+
+.row label:first-child {
+ width: 8em;
+ margin-right: 0.75em;
+ text-align: right;
+ clear: both;
+ font-weight: bold;
+ display: inline-block;
+ vertical-align: top;
+}
+
+.row textarea {
+ width: 30em;
+ height: 8em;
+}
+
+form .row .errors {
+ list-style: none;
+ margin: .5em 0 1em 10.9375em;
+ padding: 0;
+ font-size: .8em;
+ color: #c00;
+ width: 30em;
+}
+
+div.help {
+ width: 40%;
+ float: right;
+ background-color: #ffa;
+ border-top: 1px solid #ee9;
+ padding: 1em;
+ font-size: .8em;
+ margin: .625em 0;
+}
+
+div > *:first-child { margin-top: 0; }
+div > *:last-child { margin-bottom: 0; }
+
+form .buttons.indent {
+ margin-left: 8.75em;
+}
diff --git a/fedora_elections/templates/_formhelpers.html b/fedora_elections/templates/_formhelpers.html
new file mode 100644
index 0000000..ff781b7
--- /dev/null
+++ b/fedora_elections/templates/_formhelpers.html
@@ -0,0 +1,13 @@
+{% macro render_field(field, after="") %}
+<div class="row">
+ {{ field.label }}{{ field(**kwargs)|safe }}{% if after %} {{ after }}{% endif %}
+ {% if field.errors %}
+ <ul class="errors">
+ {% for error in field.errors %}
+ <li>{{ error }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+</div>
+{% endmacro %}
+{# vim: set ts=2 sw=2 sta et: #}
diff --git a/fedora_elections/templates/admin/all_elections.html b/fedora_elections/templates/admin/all_elections.html
new file mode 100644
index 0000000..b07108b
--- /dev/null
+++ b/fedora_elections/templates/admin/all_elections.html
@@ -0,0 +1,20 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>My elections</h2>
+<table>
+ <tr>
+ <th>Election</th>
+ <th>Start date (UTC)</th>
+ <th>End date (UTC)</th>
+ <th>Status</th>
+ </tr>
+ {% for election in elections %}
+ <tr>
+ <td><a href="{{ url_for('admin_view_election', election_alias=election.alias) }}">{{ election.summary }}</a></td>
+ <td>{{ election.start_date }}</td>
+ <td>{{ election.end_date }}</td>
+ <td>{{ election.status }}</td>
+ </tr>
+ {% endfor %}
+</table>
+{% endblock %}
diff --git a/fedora_elections/templates/admin/candidate.html b/fedora_elections/templates/admin/candidate.html
new file mode 100644
index 0000000..9546da0
--- /dev/null
+++ b/fedora_elections/templates/admin/candidate.html
@@ -0,0 +1,17 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}{{ submit_text }}{% endblock %}
+{% block body %}
+<h2>{{ submit_text }}</h2>
+
+<form action="" method="post">
+ <fieldset>
+ {{ render_field(form.name) }}
+ {{ render_field(form.url) }}
+ </fieldset>
+
+ <br class="clear">
+ <p class="buttons"><input type="submit" class="submit positive" value="{{ submit_text }}"></p>
+ {{ form.csrf_token }}
+</form>
+{% endblock %}
diff --git a/fedora_elections/templates/admin/delete_candidate.html b/fedora_elections/templates/admin/delete_candidate.html
new file mode 100644
index 0000000..3cfe4be
--- /dev/null
+++ b/fedora_elections/templates/admin/delete_candidate.html
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}Delete candidate{% endblock %}
+{% block body %}
+<h2>{{ self.title() }}</h2>
+
+<form action="" method="post">
+ <p>Are you sure you want to delete candidate "{{ candidate.name }}"?</p>
+ <p class="buttons"><input type="submit" value="Delete candidate">
+ <a href="{{ url_for('admin_view_election', election_alias=candidate.election.alias) }}">Cancel</a></p>
+ {{ form.csrf_token }}
+</form>
+{% endblock %}
diff --git a/fedora_elections/templates/admin/election_form.html b/fedora_elections/templates/admin/election_form.html
new file mode 100644
index 0000000..e9d05c7
--- /dev/null
+++ b/fedora_elections/templates/admin/election_form.html
@@ -0,0 +1,62 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}{{ submit_text }}{% endblock %}
+{% block body %}
+<h2>{{ self.title() }}</h2>
+
+<form action="" method="post">
+ <fieldset>
+ {{ render_field(form.summary) }}
+ </fieldset>
+ <div class="help">
+ <p>This is the name of the election that users will first see.</p>
+ <p>Example: <kbd>Fedora Board, November 2010</kbd></p>
+ </div>
+ <fieldset>
+ {{ render_field(form.alias) }}
+ </fieldset>
+ <div class="help">
+ <p>A short, unique name that will be used for the election's URL. Lowercase letters, numbers, hyphens and underscores only.</p>
+ <p>Example: <kbd>boardf15</kbd></p>
+ </div>
+ <fieldset>
+ {{ render_field(form.description) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.url) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.start_date, " at 00:00:00 UTC") }}
+ {{ render_field(form.end_date, " at 23:59:59 UTC") }}
+ </fieldset>
+ <div class="help">
+ <p>Enter dates in <kbd>YYYY-MM-DD</kbd> format.</p>
+ </div>
+ <fieldset>
+ {{ render_field(form.number_elected) }}
+ </fieldset>
+ <div class="help">
+ <p>This determines the cutoff for how many candidates win the election. For example, if you are electing four empty seats on a committee, enter <kbd>4</kbd> here.</p>
+ </div>
+ <fieldset>
+ {{ render_field(form.candidates_are_fasusers) }}
+ </fieldset>
+ <div class="help">
+ <p>Check this if every candidate in this election has a FAS user account. This will automatically pull full names and wiki page URLs from FAS.</p>
+ </div>
+ <fieldset>
+ {{ render_field(form.frontpage) }}
+ </fieldset>
+ <fieldset>
+ {{ render_field(form.embargoed) }}
+ </fieldset>
+ <div class="help">
+ <p>Embargo results when the election is finished. To unembargo them, edit the election and uncheck this box.</p>
+ </div>
+
+ <br class="clear">
+ {% block election_form_extra %}{% endblock %}
+ <p class="buttons"><input type="submit" class="submit positive" value="{{ submit_text }}"></p>
+ {{ form.csrf_token }}
+</form>
+{% endblock %}
diff --git a/fedora_elections/templates/admin/view_election.html b/fedora_elections/templates/admin/view_election.html
new file mode 100644
index 0000000..4b5b174
--- /dev/null
+++ b/fedora_elections/templates/admin/view_election.html
@@ -0,0 +1,26 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>{{ election.summary }}</h2>
+<p>{{ election.description }}</p>
+<p><a href="{{ url_for('admin_edit_election', election_alias=election.alias) }}">Edit election</a>
+<ul>
+ <li>On front page: {{ election.frontpage }}</li>
+ <li>Start date: {{ election.start_date }}</li>
+ <li>End date: {{ election.end_date }}</li>
+ <li>Number elected: {{ election.number_elected }}</li>
+ <li>Election type: {{ election.voting_type }}</li>
+</ul>
+<h3>Candidates</h3>
+{% if election.candidates.all() %}
+<ul>
+ {% for candidate in election.candidates.all() %}
+ <li>{{ candidate.name }}
+ <a href="{{ url_for('admin_edit_candidate', election_alias=election.alias, candidate_id=candidate.id) }}">[edit]</a>
+ <a href="{{ url_for('admin_delete_candidate', election_alias=election.alias, candidate_id=candidate.id) }}">[x]</a></li>
+ {% endfor %}
+</ul>
+{% else %}
+<p>There are no candidates.</p>
+{% endif %}
+<a href="{{ url_for('admin_add_candidate', election_alias=election.alias) }}">Add a candidate</a>
+{% endblock %}
diff --git a/fedora_elections/templates/auth/login.html b/fedora_elections/templates/auth/login.html
new file mode 100644
index 0000000..aafe3a8
--- /dev/null
+++ b/fedora_elections/templates/auth/login.html
@@ -0,0 +1,14 @@
+{% extends "base.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}Log in{% endblock %}
+{% block body %}
+<h2>Log in</h2>
+
+<form action="" method="post">
+ <p>Log in using your <a href="{{ config.FAS_BASE_URL }}">Fedora Accounts System</a> account.</p>
+ {{ render_field(form.username) }}
+ {{ render_field(form.password) }}
+ <p class="buttons indent"><input type="submit" class="submit positive" value="Log in"></p>
+ {{ form.csrf_token }}
+</form>
+{% endblock %}
diff --git a/fedora_elections/templates/base.html b/fedora_elections/templates/base.html
new file mode 100644
index 0000000..75f2543
--- /dev/null
+++ b/fedora_elections/templates/base.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ {% block head %}
+ <title>Fedora Elections{% if self.title() %} — {% block title %}{% endblock %}{% endif %}</title>
+ <link rel="stylesheet" href="{{ url_for("static", filename="style.css") }}">
+ {% endblock %}
+ </head>
+ <body>
+ <h1>Fedora Elections</h1>
+ {% if g.fas_user %}
+ <p>Hello, {{ g.fas_user.username }} — <a href="{{ url_for("auth_logout") }}">log out</a></p>
+ {% else %}
+ <p>You are not logged in — <a href="{{ url_for("auth_login") }}">log in</a></p>
+ {% endif %}
+ <nav>
+ <h2>Elections</h2>
+ <ul>
+ <li><a href="{{ url_for("index") }}">Open elections</a></li>
+ <li><a href="{{ url_for("archived_elections") }}">Archived elections</a></li>
+ </ul>
+ <br>
+ <h2>Administration</h2>
+ <ul>
+ <li><a href="{{ url_for("admin_view_elections") }}">My elections</a></li>
+ <li><a href="{{ url_for("admin_new_election") }}">Create new election</a></li>
+ </ul>
+ </nav>
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+ <ul id="flashes">
+ {% for message in messages %}
+ <li>{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {% endwith %}
+ {% block body %}{% endblock %}
+ </body>
+</html>
diff --git a/fedora_elections/templates/list/index.html b/fedora_elections/templates/list/index.html
new file mode 100644
index 0000000..1ad207a
--- /dev/null
+++ b/fedora_elections/templates/list/index.html
@@ -0,0 +1,4 @@
+{% extends "base.html" %}
+{% block body %}
+<p>Hello, world!</p>
+{% endblock %}
diff --git a/runserver b/runserver
new file mode 100755
index 0000000..39898a2
--- /dev/null
+++ b/runserver
@@ -0,0 +1 @@
+FEDORA_ELECTIONS_CONFIG=../development.cfg python runserver.py
diff --git a/runserver.py b/runserver.py
new file mode 100644
index 0000000..176e2b9
--- /dev/null
+++ b/runserver.py
@@ -0,0 +1,2 @@
+from fedora_elections import app
+app.run()
diff --git a/shell b/shell
new file mode 100755
index 0000000..fd475b5
--- /dev/null
+++ b/shell
@@ -0,0 +1,7 @@
+#!/bin/bash
+which ipython > /dev/null 2>&1
+if [ "$?" == "0" ]; then
+ FEDORA_ELECTIONS_CONFIG=../development.cfg ipython
+else
+ FEDORA_ELECTIONS_CONFIG=../development.cfg python
+fi
commit 6e013f2a42075c561080cee07ec64d368e5e5e4c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 13:13:41 2014 +0200
Remove everything so that we can put the flask port on the top of this
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 6401c4a..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,5 +0,0 @@
-dev.cfg
-*.pyc
-*~
-elections.egg-info
-dist
diff --git a/COPYING b/COPYING
deleted file mode 100644
index 623b625..0000000
--- a/COPYING
+++ /dev/null
@@ -1,340 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 2, June 1991
-
- Copyright (C) 1989, 1991 Free Software Foundation, Inc.
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The licenses for most software are designed to take away your
-freedom to share and change it. By contrast, the GNU General Public
-License is intended to guarantee your freedom to share and change free
-software--to make sure the software is free for all its users. This
-General Public License applies to most of the Free Software
-Foundation's software and to any other program whose authors commit to
-using it. (Some other Free Software Foundation software is covered by
-the GNU Library General Public License instead.) You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-this service if you wish), that you receive source code or can get it
-if you want it, that you can change the software or use pieces of it
-in new free programs; and that you know you can do these things.
-
- To protect your rights, we need to make restrictions that forbid
-anyone to deny you these rights or to ask you to surrender the rights.
-These restrictions translate to certain responsibilities for you if you
-distribute copies of the software, or if you modify it.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must give the recipients all the rights that
-you have. You must make sure that they, too, receive or can get the
-source code. And you must show them these terms so they know their
-rights.
-
- We protect your rights with two steps: (1) copyright the software, and
-(2) offer you this license which gives you legal permission to copy,
-distribute and/or modify the software.
-
- Also, for each author's protection and ours, we want to make certain
-that everyone understands that there is no warranty for this free
-software. If the software is modified by someone else and passed on, we
-want its recipients to know that what they have is not the original, so
-that any problems introduced by others will not reflect on the original
-authors' reputations.
-
- Finally, any free program is threatened constantly by software
-patents. We wish to avoid the danger that redistributors of a free
-program will individually obtain patent licenses, in effect making the
-program proprietary. To prevent this, we have made it clear that any
-patent must be licensed for everyone's free use or not licensed at all.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- GNU GENERAL PUBLIC LICENSE
- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
- 0. This License applies to any program or other work which contains
-a notice placed by the copyright holder saying it may be distributed
-under the terms of this General Public License. The "Program", below,
-refers to any such program or work, and a "work based on the Program"
-means either the Program or any derivative work under copyright law:
-that is to say, a work containing the Program or a portion of it,
-either verbatim or with modifications and/or translated into another
-language. (Hereinafter, translation is included without limitation in
-the term "modification".) Each licensee is addressed as "you".
-
-Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope. The act of
-running the Program is not restricted, and the output from the Program
-is covered only if its contents constitute a work based on the
-Program (independent of having been made by running the Program).
-Whether that is true depends on what the Program does.
-
- 1. You may copy and distribute verbatim copies of the Program's
-source code as you receive it, in any medium, provided that you
-conspicuously and appropriately publish on each copy an appropriate
-copyright notice and disclaimer of warranty; keep intact all the
-notices that refer to this License and to the absence of any warranty;
-and give any other recipients of the Program a copy of this License
-along with the Program.
-
-You may charge a fee for the physical act of transferring a copy, and
-you may at your option offer warranty protection in exchange for a fee.
-
- 2. You may modify your copy or copies of the Program or any portion
-of it, thus forming a work based on the Program, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
- a) You must cause the modified files to carry prominent notices
- stating that you changed the files and the date of any change.
-
- b) You must cause any work that you distribute or publish, that in
- whole or in part contains or is derived from the Program or any
- part thereof, to be licensed as a whole at no charge to all third
- parties under the terms of this License.
-
- c) If the modified program normally reads commands interactively
- when run, you must cause it, when started running for such
- interactive use in the most ordinary way, to print or display an
- announcement including an appropriate copyright notice and a
- notice that there is no warranty (or else, saying that you provide
- a warranty) and that users may redistribute the program under
- these conditions, and telling the user how to view a copy of this
- License. (Exception: if the Program itself is interactive but
- does not normally print such an announcement, your work based on
- the Program is not required to print an announcement.)
-
-These requirements apply to the modified work as a whole. If
-identifiable sections of that work are not derived from the Program,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works. But when you
-distribute the same sections as part of a whole which is a work based
-on the Program, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Program.
-
-In addition, mere aggregation of another work not based on the Program
-with the Program (or with a work based on the Program) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
- 3. You may copy and distribute the Program (or a work based on it,
-under Section 2) in object code or executable form under the terms of
-Sections 1 and 2 above provided that you also do one of the following:
-
- a) Accompany it with the complete corresponding machine-readable
- source code, which must be distributed under the terms of Sections
- 1 and 2 above on a medium customarily used for software interchange; or,
-
- b) Accompany it with a written offer, valid for at least three
- years, to give any third party, for a charge no more than your
- cost of physically performing source distribution, a complete
- machine-readable copy of the corresponding source code, to be
- distributed under the terms of Sections 1 and 2 above on a medium
- customarily used for software interchange; or,
-
- c) Accompany it with the information you received as to the offer
- to distribute corresponding source code. (This alternative is
- allowed only for noncommercial distribution and only if you
- received the program in object code or executable form with such
- an offer, in accord with Subsection b above.)
-
-The source code for a work means the preferred form of the work for
-making modifications to it. For an executable work, complete source
-code means all the source code for all modules it contains, plus any
-associated interface definition files, plus the scripts used to
-control compilation and installation of the executable. However, as a
-special exception, the source code distributed need not include
-anything that is normally distributed (in either source or binary
-form) with the major components (compiler, kernel, and so on) of the
-operating system on which the executable runs, unless that component
-itself accompanies the executable.
-
-If distribution of executable or object code is made by offering
-access to copy from a designated place, then offering equivalent
-access to copy the source code from the same place counts as
-distribution of the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
- 4. You may not copy, modify, sublicense, or distribute the Program
-except as expressly provided under this License. Any attempt
-otherwise to copy, modify, sublicense or distribute the Program is
-void, and will automatically terminate your rights under this License.
-However, parties who have received copies, or rights, from you under
-this License will not have their licenses terminated so long as such
-parties remain in full compliance.
-
- 5. You are not required to accept this License, since you have not
-signed it. However, nothing else grants you permission to modify or
-distribute the Program or its derivative works. These actions are
-prohibited by law if you do not accept this License. Therefore, by
-modifying or distributing the Program (or any work based on the
-Program), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Program or works based on it.
-
- 6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the
-original licensor to copy, distribute or modify the Program subject to
-these terms and conditions. You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties to
-this License.
-
- 7. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Program at all. For example, if a patent
-license would not permit royalty-free redistribution of the Program by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Program.
-
-If any portion of this section is held invalid or unenforceable under
-any particular circumstance, the balance of the section is intended to
-apply and the section as a whole is intended to apply in other
-circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system, which is
-implemented by public license practices. Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
- 8. If the distribution and/or use of the Program is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Program under this License
-may add an explicit geographical distribution limitation excluding
-those countries, so that distribution is permitted only in or among
-countries not thus excluded. In such case, this License incorporates
-the limitation as if written in the body of this License.
-
- 9. The Free Software Foundation may publish revised and/or new versions
-of the General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-Each version is given a distinguishing version number. If the Program
-specifies a version number of this License which applies to it and "any
-later version", you have the option of following the terms and conditions
-either of that version or of any later version published by the Free
-Software Foundation. If the Program does not specify a version number of
-this License, you may choose any version ever published by the Free Software
-Foundation.
-
- 10. If you wish to incorporate parts of the Program into other free
-programs whose distribution conditions are different, write to the author
-to ask for permission. For software which is copyrighted by the Free
-Software Foundation, write to the Free Software Foundation; we sometimes
-make exceptions for this. Our decision will be guided by the two goals
-of preserving the free status of all derivatives of our free software and
-of promoting the sharing and reuse of software generally.
-
- NO WARRANTY
-
- 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
-FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
-OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
-PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
-OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
-TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
-PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
-REPAIR OR CORRECTION.
-
- 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
-REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
-INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
-OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
-TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
-YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
-PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGES.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
- <one line to give the program's name and a brief idea of what it does.>
- Copyright (C) <year> <name of author>
-
- This program is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program; if not, write to the Free Software
- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-
-Also add information on how to contact you by electronic and paper mail.
-
-If the program is interactive, make it output a short notice like this
-when it starts in an interactive mode:
-
- Gnomovision version 69, Copyright (C) year name of author
- Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, the commands you use may
-be called something other than `show w' and `show c'; they could even be
-mouse-clicks or menu items--whatever suits your program.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the program, if
-necessary. Here is a sample; alter the names:
-
- Yoyodyne, Inc., hereby disclaims all copyright interest in the program
- `Gnomovision' (which makes passes at compilers) written by James Hacker.
-
- <signature of Ty Coon>, 1 April 1989
- Ty Coon, President of Vice
-
-This General Public License does not permit incorporating your program into
-proprietary programs. If your program is a subroutine library, you may
-consider it more useful to permit linking proprietary applications with the
-library. If this is what you want to do, use the GNU Library General
-Public License instead of this License.
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 85ec67e..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,6 +0,0 @@
-recursive-include elections2/public *
-include elections2/public/favicon.ico
-recursive-include elections2/i18n *
-recursive-include elections2/templates *
-include *.spec
-*.ini
diff --git a/README b/README
deleted file mode 100644
index 6ecb5c5..0000000
--- a/README
+++ /dev/null
@@ -1,64 +0,0 @@
-Documentation
-
-* What is fedora-election?
-fedora-elections is a web application written in python and based on
-TurboGears which aim to help to make decisions where is necessary to
-give a preferences.
-This project is developed over fedoraproject requests, but,
-can be easily adapt to be used from any other association/project.
-Fedora-elections is integrated with Fedora Account System (FAS) and
-provides an agile and easy interface to make decision using votes.
-
-
-* Installation
-You can get files by svn repository. You need a valid FAS account in
-particular you need to upload your ssh public key because svn require
-authentication also to checkout/update.
-
-Before launch fedora-elections, you need this packages installed on your
-machine:
- - postgresql-server
- - TurboGears
- - CherryPy
- - python-fedora (you need to enable fedora testing-updates repository)
-
-Before start fedora elections you need to set-up property your database.
-
-Copy your dev-dist.cfg to dev.cfg and start to edit it. This is your
-development configuration file. prod.cfg is your Production configuration
-file.
-
- - sqlite
-SQLite is useful to quick start because doesn't need any type of
-configuration.
-You have just to edit dev.cfg and make sure you have a line like this:
-
- sqlalchemy.dburi="sqlite://path_of_your_database"
-
- - PostgreSQL
-First of all you need to create and configure an user and database
-and give permission on it.
-
-su - postgres
-createuse felections
-createdb -O felections felections
-psql felections felection < /path/to/election/trunk/elections.sql
-
-(don't forget to edit your pg_sql.conf data and assign to felections on
-your host)
-
-* How to start
-Fedora-elections can be started by running the
-start-elections.py script.
-
-
-* How to contribute
-If you find bug or want to propose ideas or stuff to be implemented or if
-you are interested to became a developer for this project just
-ask on #fedora-admin irc channel on irc.freenode.net or use our
-web site https://fedorahosted.org/elections.
-
-* Licence
-fedora-elections is licenced under GPL v2.
-
-
diff --git a/README.txt b/README.txt
deleted file mode 100644
index e3e1643..0000000
--- a/README.txt
+++ /dev/null
@@ -1,44 +0,0 @@
-This file is for you to describe the elections2 application. Typically
-you would include information such as the information below:
-
-Dependencies
-============
-
-- TurboGears 2
- tested with:
- - TurboGears2-2.0.3-4.el6
- - TurboGears2-2.1-0.3.rc1.dev1048.fc14
-- python-sqlalchemy
- tested with:
- - python-sqlalchemy-0.5.5-2.1.el6
- - python-sqlalchemy-0.6.8-1.fc14
-- python-tgext-admin
-- python-repoze-what-quickstart
-- python-tgext-crud
-- python-sprox
-
-
-Installation and Setup
-======================
-
-Install ``elections2`` using the setup.py script::
-
- $ cd elections2
- $ python setup.py install
-
-Create the egg_info
- $ python setup.py egg_info
-
-Create the project database for any model classes defined::
-
- $ paster setup-app development.ini
-
-Start the paste http server::
-
- $ paster serve development.ini
-
-While developing you may want the server to reload after changes in package files (or its dependencies) are saved. This can be achieved easily by adding the --reload option::
-
- $ paster serve --reload development.ini
-
-Then you are ready to go.
diff --git a/UPDATE-NOTES b/UPDATE-NOTES
deleted file mode 100644
index 8b13789..0000000
--- a/UPDATE-NOTES
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/development.ini b/development.ini
deleted file mode 100644
index 7854255..0000000
--- a/development.ini
+++ /dev/null
@@ -1,135 +0,0 @@
-#
-# elections2 - Pylons development environment configuration
-#
-# The %(here)s variable will be replaced with the parent directory of this file
-#
-# This file is for deployment specific config options -- other configuration
-# that is always required for the app is done in the config directory,
-# and generally should not be modified by end users.
-
-[DEFAULT]
-debug = true
-# Uncomment and replace with the address which should receive any error reports
-#email_to = you(a)yourdomain.com
-smtp_server = localhost
-error_email_from = paste@localhost
-
-[server:main]
-use = egg:Paste#http
-host = 127.0.0.1
-port = 8080
-
-[sa_auth]
-cookie_secret = d0dc7a4f-1d03-4e22-a6f6-ecd21b8a6464
-
-[app:main]
-use = egg:elections2
-full_stack = true
-#lang = ru
-cache_dir = %(here)s/data
-beaker.session.key = elections2
-beaker.session.secret = d0dc7a4f-1d03-4e22-a6f6-ecd21b8a6464
-
-# If you'd like to fine-tune the individual locations of the cache data dirs
-# for the Cache data, or the Session saves, un-comment the desired settings
-# here:
-#beaker.cache.data_dir = %(here)s/data/cache
-#beaker.session.data_dir = %(here)s/data/sessions
-
-# pick the form for your database
-# %(here) may include a ':' character on Windows environments; this can
-# invalidate the URI when specifying a SQLite db via path name
-# sqlalchemy.url=postgres://username:password@hostname:port/databasename
-# sqlalchemy.url=mysql://username:password@hostname:port/databasename
-
-
-# If you have sqlite, here's a simple default to get you started
-# in development
-
-sqlalchemy.url = sqlite:///%(here)s/devdata.db
-#sqlalchemy.url=postgresql://elections:elections2@localhost/elections
-#echo shouldn't be used together with the logging module.
-sqlalchemy.echo = false
-sqlalchemy.echo_pool = false
-sqlalchemy.pool_recycle = 3600
-
-# if you are using Mako and want to be able to reload
-# the mako template from disk during the development phase
-# you should say 'true' here
-# This option is only used for mako templating engine
-# WARNING: if you want to deploy your application using a zipped egg
-# (ie: if your application's setup.py defines zip-safe=True, then you
-# MUST put "false" for the production environment because there will
-# be no disk and real files to compare time with.
-# On the contrary if your application defines zip-safe=False and is
-# deployed in an unzipped manner, then you can leave this option to true
-templating.mako.reloadfromdisk = true
-
-# the compiled template dir is a directory that must be readable by your
-# webserver. It will be used to store the resulting templates once compiled
-# by the TemplateLookup system.
-# During development you generally don't need this option since paste's HTTP
-# server will have access to you development directories, but in production
-# you'll most certainly want to have apache or nginx to write in a directory
-# that does not contain any source code in any form for obvious security reasons.
-templating.mako.compiled_templates_dir = %(here)s/data/templates
-
-# WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*
-# Debug mode will enable the interactive debugging tool, allowing ANYONE to
-# execute malicious code after an exception is raised.
-#set debug = false
-
-# Set to a group that is allowed to administrate all elections
-admingroup = 'elections'
-
-# Logging configuration
-# Add additional loggers, handlers, formatters here
-# Uses python's logging config file format
-# http://docs.python.org/lib/logging-config-fileformat.html
-
-[loggers]
-keys = root, elections2, sqlalchemy, auth
-
-[handlers]
-keys = console
-
-[formatters]
-keys = generic
-
-# If you create additional loggers, add them as a key to [loggers]
-[logger_root]
-level = INFO
-handlers = console
-
-[logger_elections2]
-level = DEBUG
-handlers =
-qualname = elections2
-
-[logger_sqlalchemy]
-level = INFO
-handlers =
-qualname = sqlalchemy.engine
-# "level = INFO" logs SQL queries.
-# "level = DEBUG" logs SQL queries and results.
-# "level = WARN" logs neither. (Recommended for production systems.)
-
-
-# A logger for authentication, identification and authorization -- this is
-# repoze.who and repoze.what:
-[logger_auth]
-level = WARN
-handlers =
-qualname = auth
-
-# If you create additional handlers, add them as a key to [handlers]
-[handler_console]
-class = StreamHandler
-args = (sys.stderr,)
-level = NOTSET
-formatter = generic
-
-# If you create additional formatters, add them as a key to [formatters]
-[formatter_generic]
-format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
-datefmt = %H:%M:%S
diff --git a/elections.sql b/elections.sql
deleted file mode 100644
index 481e500..0000000
--- a/elections.sql
+++ /dev/null
@@ -1,90 +0,0 @@
-drop view electionvotes;
-drop view fvotecount;
-drop view votecount;
-drop view uservotes;
-
-drop table votes;
-drop table legalvoters;
-drop table candidates;
-drop table elections;
-
-CREATE TABLE elections (
- id serial NOT NULL,
--- was shortname
- shortdesc text NOT NULL,
--- was name
- alias text NOT NULL,
--- was info
- description text NOT NULL,
- url text NOT NULL,
- start_date timestamp without time zone NOT NULL,
- end_date timestamp without time zone NOT NULL,
--- was max_seats
- seats_elected integer NOT NULL,
- votes_per_user integer NOT NULL,
- embargoed integer default 0 NOT NULL,
- usefas integer default 0 NOT NULL,
- allow_nominations integer default 0 NOT NULL,
- nominations_until timestamp without time zone,
- unique (shortdesc),
- primary key (id)
-);
-
-CREATE TABLE candidates (
- id serial,
- election_id integer NOT NULL,
- name text NOT NULL,
- url text,
- formalname text,
- human integer,
- status integer,
- foreign key (election_id) references elections (id),
- unique (id),
- primary key (id, election_id)
-);
-
-CREATE TABLE legalvoters (
- id serial,
- election_id integer not null,
- group_name text not null,
- foreign key (election_id) references elections (id),
- primary key (id)
-);
-
-CREATE TABLE electionadmins (
- id serial,
- election_id integer not null,
- group_name text not null,
- foreign key (election_id) references elections (id),
- primary key (id)
-);
-
-CREATE TABLE votes (
- id serial,
- voter text NOT NULL,
- timestamp timestamp without time zone NOT NULL,
- candidate_id integer NOT NULL,
- weight integer NOT NULL,
- election_id integer NOT NULL,
- foreign key (candidate_id) references candidates(id),
- foreign key (election_id) references elections(id),
- primary key (id)
-);
-
-
-CREATE VIEW electionvotes AS
- SELECT votes.election_id, count(votes.election_id) AS novotes FROM votes GROUP BY votes.election_id;
-
-
-
-
-CREATE VIEW votecount AS
- SELECT votes.candidate_id, votes.election_id, sum(votes.weight) AS novotes FROM votes GROUP BY votes.candidate_id, votes.election_id ORDER BY sum(votes.weight) DESC;
-
-
-CREATE VIEW fvotecount AS
- SELECT c.id, c.name, v.election_id, v.novotes FROM votecount v, candidates c WHERE (c.id = v.candidate_id) ORDER BY v.novotes DESC;
-
-
-CREATE VIEW uservotes AS
- SELECT votes.election_id, votes.voter, count(votes.voter) AS novotes FROM votes GROUP BY votes.election_id, votes.voter;
diff --git a/elections2/__init__.py b/elections2/__init__.py
deleted file mode 100644
index 05d80e0..0000000
--- a/elections2/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# -*- coding: utf-8 -*-
-"""The elections2 package"""
diff --git a/elections2/config/__init__.py b/elections2/config/__init__.py
deleted file mode 100644
index 633f866..0000000
--- a/elections2/config/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# -*- coding: utf-8 -*-
-
diff --git a/elections2/config/app_cfg.py b/elections2/config/app_cfg.py
deleted file mode 100644
index 0c19a7d..0000000
--- a/elections2/config/app_cfg.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Global configuration file for TG2-specific settings in elections2.
-
-This file complements development/deployment.ini.
-
-Please note that **all the argument values are strings**. If you want to
-convert them into boolean, for example, you should use the
-:func:`paste.deploy.converters.asbool` function, as in::
-
- from paste.deploy.converters import asbool
- setting = asbool(global_conf.get('the_setting'))
-
-"""
-
-from tg.configuration import AppConfig
-
-import elections2
-from elections2 import model
-from elections2.lib import app_globals, helpers
-from fedora.tg2.utils import add_fas_auth_middleware
-
-class MyAppConfig(AppConfig):
- add_auth_middleware = add_fas_auth_middleware
-
-base_config = MyAppConfig()
-#base_config = AppConfig()
-
-base_config.renderers = []
-
-base_config.package = elections2
-
-#Enable json in expose
-base_config.renderers.append('json')
-#Set the default renderer
-base_config.default_renderer = 'genshi'
-base_config.renderers.append('genshi')
-# if you want raw speed and have installed chameleon.genshi
-# you should try to use this renderer instead.
-# warning: for the moment chameleon does not handle i18n translations
-#base_config.renderers.append('chameleon_genshi')
-
-#Configure the base SQLALchemy Setup
-base_config.use_sqlalchemy = True
-base_config.model = elections2.model
-base_config.DBSession = elections2.model.DBSession
-
-# Configure the authentication backend
-
-# YOU MUST CHANGE THIS VALUE IN PRODUCTION TO SECURE YOUR APP
-base_config.sa_auth.cookie_secret = "iqy4o#iyu7foi7y384tyorthifvy78q*@⁾$"
-
-base_config.auth_backend = 'sqlalchemy'
-base_config.sa_auth.dbsession = model.DBSession
-# what is the class you want to use to search for users in the database
-base_config.sa_auth.user_class = model.User
-# what is the class you want to use to search for groups in the database
-base_config.sa_auth.group_class = model.Group
-# what is the class you want to use to search for permissions in the database
-base_config.sa_auth.permission_class = model.Permission
-
-# override this if you would like to provide a different who plugin for
-# managing login and logout of your application
-base_config.sa_auth.form_plugin = None
-
-# override this if you are using a different charset for the login form
-base_config.sa_auth.charset = 'utf-8'
-
-# You may optionally define a page where you want users to be redirected to
-# on login:
-base_config.sa_auth.post_login_url = '/post_login'
-
-# You may optionally define a page where you want users to be redirected to
-# on logout:
-base_config.sa_auth.post_logout_url = '/post_logout'
diff --git a/elections2/config/deployment.ini b/elections2/config/deployment.ini
deleted file mode 100644
index d2e3adb..0000000
--- a/elections2/config/deployment.ini
+++ /dev/null
@@ -1,99 +0,0 @@
-#
-# elections2 - TurboGears configuration
-#
-# The %(here)s variable will be replaced with the parent directory of this file
-#
-[DEFAULT]
-# WARGING == If debug is not set to false, you'll get the interactive
-# debugger on production, which is a huge security hole.
-
-debug = false
-email_to = pingou(a)pingoured.fr
-smtp_server = localhost
-error_email_from = tg2(a)cyan.pingoured.fr
-
-[server:main]
-use = egg:Paste#http
-host = 0.0.0.0
-port = 8080
-
-[sa_auth]
-cookie_secret = ${app_instance_secret}
-
-[app:main]
-use = egg:elections2
-full_stack = true
-cache_dir = %(here)s/data
-beaker.session.key = elections2
-beaker.session.secret = ${app_instance_secret}
-app_instance_uuid = ${app_instance_uuid}
-
-# If you'd like to fine-tune the individual locations of the cache data dirs
-# for the Cache data, or the Session saves, un-comment the desired settings
-# here:
-#beaker.cache.data_dir = %(here)s/data/cache
-#beaker.session.data_dir = %(here)s/data/sessions
-# Specify the database for SQLAlchemy to use via
-# turbogears.database
-# %(here) may include a ':' character on Windows environments; this can
-# invalidate the URI when specifying a SQLite db via path name
-#sqlalchemy.url = sqlite:///%(here)s/somedb.db
-sqlalchemy.url="postgres://election@localhost/elections2"
-sqlalchemy.echo = False
-
-# WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*
-# Debug mode will enable the interactive debugging tool, allowing ANYONE to
-# execute malicious code after an exception is raised.
-#set debug = false
-
-# Logging configuration
-# Add additional loggers, handlers, formatters here
-# Uses python's logging config file format
-# http://docs.python.org/lib/logging-config-fileformat.html
-
-[loggers]
-keys = root, elections2, sqlalchemy, auth
-
-[handlers]
-keys = console
-
-[formatters]
-keys = generic
-
-# If you create additional loggers, add them as a key to [loggers]
-[logger_root]
-level = INFO
-handlers = console
-
-[logger_elections2]
-level = INFO
-handlers =
-qualname = elections2
-
-[logger_sqlalchemy]
-level = WARN
-handlers =
-qualname = sqlalchemy.engine
-# "level = INFO" logs SQL queries.
-# "level = DEBUG" logs SQL queries and results.
-# "level = WARN" logs neither. (Recommended for production systems.)
-
-
-# A logger for authentication, identification and authorization -- this is
-# repoze.who and repoze.what:
-[logger_auth]
-level = WARN
-handlers =
-qualname = auth
-
-# If you create additional handlers, add them as a key to [handlers]
-[handler_console]
-class = StreamHandler
-args = (sys.stderr,)
-level = NOTSET
-formatter = generic
-
-# If you create additional formatters, add them as a key to [formatters]
-[formatter_generic]
-format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
-datefmt = %H:%M:%S
diff --git a/elections2/config/deployment.ini_tmpl b/elections2/config/deployment.ini_tmpl
deleted file mode 100644
index 0b0d67d..0000000
--- a/elections2/config/deployment.ini_tmpl
+++ /dev/null
@@ -1,98 +0,0 @@
-#
-# elections2 - TurboGears configuration
-#
-# The %(here)s variable will be replaced with the parent directory of this file
-#
-[DEFAULT]
-# WARGING == If debug is not set to false, you'll get the interactive
-# debugger on production, which is a huge security hole.
-
-debug = false
-email_to = you(a)yourdomain.com
-smtp_server = localhost
-error_email_from = paste@localhost
-
-[server:main]
-use = egg:Paste#http
-host = 0.0.0.0
-port = 8080
-
-[sa_auth]
-cookie_secret = ${app_instance_secret}
-
-[app:main]
-use = egg:elections2
-full_stack = true
-cache_dir = %(here)s/data
-beaker.session.key = elections2
-beaker.session.secret = ${app_instance_secret}
-app_instance_uuid = ${app_instance_uuid}
-
-# If you'd like to fine-tune the individual locations of the cache data dirs
-# for the Cache data, or the Session saves, un-comment the desired settings
-# here:
-#beaker.cache.data_dir = %(here)s/data/cache
-#beaker.session.data_dir = %(here)s/data/sessions
-# Specify the database for SQLAlchemy to use via
-# turbogears.database
-# %(here) may include a ':' character on Windows environments; this can
-# invalidate the URI when specifying a SQLite db via path name
-sqlalchemy.url = sqlite:///%(here)s/somedb.db
-sqlalchemy.echo = False
-
-# WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*
-# Debug mode will enable the interactive debugging tool, allowing ANYONE to
-# execute malicious code after an exception is raised.
-#set debug = false
-
-# Logging configuration
-# Add additional loggers, handlers, formatters here
-# Uses python's logging config file format
-# http://docs.python.org/lib/logging-config-fileformat.html
-
-[loggers]
-keys = root, elections2, sqlalchemy, auth
-
-[handlers]
-keys = console
-
-[formatters]
-keys = generic
-
-# If you create additional loggers, add them as a key to [loggers]
-[logger_root]
-level = INFO
-handlers = console
-
-[logger_elections2]
-level = INFO
-handlers =
-qualname = elections2
-
-[logger_sqlalchemy]
-level = WARN
-handlers =
-qualname = sqlalchemy.engine
-# "level = INFO" logs SQL queries.
-# "level = DEBUG" logs SQL queries and results.
-# "level = WARN" logs neither. (Recommended for production systems.)
-
-
-# A logger for authentication, identification and authorization -- this is
-# repoze.who and repoze.what:
-[logger_auth]
-level = WARN
-handlers =
-qualname = auth
-
-# If you create additional handlers, add them as a key to [handlers]
-[handler_console]
-class = StreamHandler
-args = (sys.stderr,)
-level = NOTSET
-formatter = generic
-
-# If you create additional formatters, add them as a key to [formatters]
-[formatter_generic]
-format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
-datefmt = %H:%M:%S
diff --git a/elections2/config/environment.py b/elections2/config/environment.py
deleted file mode 100644
index 32bd8c2..0000000
--- a/elections2/config/environment.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# -*- coding: utf-8 -*-
-"""WSGI environment setup for elections2."""
-
-from elections2.config.app_cfg import base_config
-
-__all__ = ['load_environment']
-
-#Use base_config to setup the environment loader function
-load_environment = base_config.make_load_environment()
diff --git a/elections2/config/middleware.py b/elections2/config/middleware.py
deleted file mode 100644
index ca09565..0000000
--- a/elections2/config/middleware.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# -*- coding: utf-8 -*-
-"""WSGI middleware initialization for the elections2 application."""
-
-from elections2.config.app_cfg import base_config
-from elections2.config.environment import load_environment
-
-
-__all__ = ['make_app']
-
-# Use base_config to setup the necessary PasteDeploy application factory.
-# make_base_app will wrap the TG2 app with all the middleware it needs.
-make_base_app = base_config.setup_tg_wsgi_app(load_environment)
-
-
-def make_app(global_conf, full_stack=True, **app_conf):
- """
- Set elections2 up with the settings found in the PasteDeploy configuration
- file used.
-
- :param global_conf: The global settings for elections2 (those
- defined under the ``[DEFAULT]`` section).
- :type global_conf: dict
- :param full_stack: Should the whole TG2 stack be set up?
- :type full_stack: str or bool
- :return: The elections2 application with all the relevant middleware
- loaded.
-
- This is the PasteDeploy factory for the elections2 application.
-
- ``app_conf`` contains all the application-specific settings (those defined
- under ``[app:main]``.
-
-
- """
- app = make_base_app(global_conf, full_stack=True, **app_conf)
-
- # Wrap your base TurboGears 2 application with custom middleware here
-
- return app
diff --git a/elections2/controllers/__init__.py b/elections2/controllers/__init__.py
deleted file mode 100644
index 80df58a..0000000
--- a/elections2/controllers/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Controllers for the elections2 application."""
diff --git a/elections2/controllers/controller.template b/elections2/controllers/controller.template
deleted file mode 100644
index afbe3b9..0000000
--- a/elections2/controllers/controller.template
+++ /dev/null
@@ -1,23 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Sample controller module"""
-
-# turbogears imports
-from tg import expose
-#from tg import redirect, validate, flash
-
-# third party imports
-#from pylons.i18n import ugettext as _
-#from repoze.what import predicates
-
-# project specific imports
-from elections2.lib.base import BaseController
-#from elections2.model import DBSession, metadata
-
-
-class SampleController(BaseController):
- #Uncomment this line if your controller requires an authenticated user
- #allow_only = authorize.not_anonymous()
-
- @expose('elections2.templates.index')
- def index(self):
- return dict(page='index')
diff --git a/elections2/controllers/error.py b/elections2/controllers/error.py
deleted file mode 100644
index 8cf6074..0000000
--- a/elections2/controllers/error.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Error controller"""
-
-from tg import request, expose
-
-__all__ = ['ErrorController']
-
-
-class ErrorController(object):
- """
- Generates error documents as and when they are required.
-
- The ErrorDocuments middleware forwards to ErrorController when error
- related status codes are returned from the application.
-
- This behaviour can be altered by changing the parameters to the
- ErrorDocuments middleware in your config/middleware.py file.
-
- """
-
- @expose('elections2.templates.error')
- def document(self, *args, **kwargs):
- """Render the error document"""
- resp = request.environ.get('pylons.original_response')
- default_message = ("<p>We're sorry but we weren't able to process "
- " this request.</p>")
- values = dict(prefix=request.environ.get('SCRIPT_NAME', ''),
- code=request.params.get('code', resp.status_int),
- message=request.params.get('message', default_message))
- return values
diff --git a/elections2/controllers/root.py b/elections2/controllers/root.py
deleted file mode 100644
index ca672a3..0000000
--- a/elections2/controllers/root.py
+++ /dev/null
@@ -1,284 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Main Controller"""
-
-from tg import config, expose, flash, require, url, request, redirect
-from pylons.i18n import ugettext as _, lazy_ugettext as l_
-from tgext.admin.tgadminconfig import TGAdminConfig
-from tgext.admin.controller import AdminController
-from repoze.what import predicates
-
-from elections2.lib.base import BaseController
-from elections2.model import DBSession, metadata
-from elections2.controllers.error import ErrorController
-from elections2 import model
-from elections2.controllers.secure import SecureController
-
-from fedora.client import AuthError, AppError
-from fedora.client.fas2 import AccountSystem
-
-from elections2 import model
-from elections2.model import *
-from elections2.lib.admin import Admin
-from elections2.lib.vote import Vote
-
-
-from datetime import datetime
-from sqlalchemy import func
-
-__all__ = ['RootController']
-
-
-class RootController(BaseController):
- """
- The root controller for the elections2 application.
-
- All the other controllers and WSGI applications should be mounted on this
- controller. For example::
-
- panel = ControlPanelController()
- another_app = AnotherWSGIApplication()
-
- Keep in mind that WSGI applications shouldn't be mounted directly: They
- must be wrapped around with :class:`tg.controllers.WSGIAppController`.
-
- """
- secc = SecureController()
-
- #admin = AdminController(model, DBSession, config_type=TGAdminConfig)
-
- error = ErrorController()
-
- appTitle = 'Fedora Elections'
-
- baseURL = "https://admin.fedoraproject.org/accounts"
- username = "elections"
- password = "elections2"
- fas = AccountSystem(baseURL, username=username, password=password)
-
- admin = Admin(fas, appTitle)
- vote = Vote(fas, appTitle)
-
- request.identity = request.environ.get('repoze.who.identity')
-
- @expose('elections2.templates.index')
- def index(self):
- """Handle the front-page."""
- if request.identity:
- userid = request.identity['repoze.who.userid']
- flash(_('Welcome back, %s!') % userid)
-
- elections = model.DBSession.query(Elections).order_by(
- Elections.start_date).filter('id>0').all()
- past = []
- current = []
- future = []
- now = datetime.utcnow()
- for e in elections:
- if e.start_date > now :
- future.append(e)
- elif e.end_date < now :
- past.append(e)
- else :
- current.append(e)
- return dict(past=past, current=current, future=future,
- curtime=datetime.utcnow(), appTitle=self.appTitle)
-
- @expose('elections2.templates.about')
- def about(self, eid=None):
- """Handle the 'about' page."""
- try:
- eid = int(eid)
- election = model.DBSession.query(Elections
- ).filter_by(id=eid).all()[0]
- except ValueError:
- try:
- election = model.DBSession.query(Elections
- ).filter_by(alias=eid).all()[0]
- eid = election.id
- except IndexError:
- flash("This election does not exist, check" \
- " if you have used the correct URL.")
- raise redirect("/")
- except (IndexError, TypeError):
- flash("This election does not exist, check if" \
- " you have used the correct URL.")
- raise redirect("/")
-
- candidates = model.DBSession.query(Candidates
- ).filter_by(election_id=election.id).all()
- votergroups = model.DBSession.query(LegalVoters
- ).filter_by(election_id=election.id).all()
- usernamemap = {}
- groupnamemap = {}
-
- if election.usefas:
- for c in candidates:
- try:
- usernamemap[c.id] = \
- self.fas.person_by_username(c.name)['human_name']
- except (KeyError, AuthError):
- # User has their name set to private or user doesn't exist
- usernamemap[c.id] = c.name
- for g in votergroups:
- try:
- groupnamemap[g.group_name] = g.group_name + " (" + \
- self.fas.group_by_name(g.group_name)['display_name'] +")"
- except (AppError, AuthError, KeyError) :
- groupnamemap[g.group_name] = g.group_name
-
- curtime = datetime.utcnow()
-
- return dict(eid=eid, candidates=candidates,
- usernamemap=usernamemap, election=election,
- curtime=curtime, votergroups=votergroups,
- appTitle=self.appTitle, groupnamemap=groupnamemap)
-
- @expose(template="elections2.templates.verify")
- @require(predicates.not_anonymous("Please log-in"))
- def verify(self):
- validvotes = {}
- invalidvotes = {}
- c = 0
- print request.identity['user']
- allvotes = model.DBSession.query(Votes.election_id,
- Votes.voter, func.count(Votes.voter)
- ).filter_by(voter=request.identity['user']
- ).group_by(Votes.voter).all()
- for v in allvotes:
- if len(v.election.candidates) == v.novotes:
- validvotes[c] = v
- c=c+1
- else:
- invalidvotes[c] = v
- c=c+1
- return dict(validvotes=validvotes, invalidvotes=invalidvotes,
- appTitle=self.appTitle)
-
- #@expose(allow_json=True)
- @expose()
- def logout(self):
- return fc_logout()
-
- @expose('elections2.templates.login')
- def login(self, came_from=url('/'), csrf_login = None):
- """Start the user login."""
- # code from elections v1
- #login_dict = fc_login(forward_url, args, kwargs)
- login_dict = {}
- login_dict['appTitle'] = '%s -- Fedora Account System Login' % \
- self.appTitle
- login_dict['message'] = _("Please log-in")
- login_dict['previous_url'] = came_from
- login_dict['forward_url'] = "/post_login"
- login_dict['original_parameters'] = request.params
-
- login_counter = request.environ['repoze.who.logins']
- if login_counter > 0:
- flash(_('Wrong credentials'), 'warning')
- if login_counter > 5:
- flash(_('Too many wrong attempts -- Blocked user'), 'warning')
- login_dict['login_counter'] = str(login_counter)
- login_dict['page'] = 'login'
- login_dict['came_from'] = came_from
-
- return login_dict
-
- ## Default code from TG2
- #login_counter = request.environ['repoze.who.logins']
- #return dict(page='login', login_counter=str(login_counter),
- #came_from=came_from)
-
- @expose(template="elections2.templates.results")
- def results(self, eid=None):
- try:
- eid = int(eid)
- election = model.DBSession.query(Elections).filter_by(
- id=eid).all()[0]
- except ValueError:
- try:
- election = model.DBSession.query(Elections).filter_by(
- alias=eid).all()[0]
- eid = election.id
- except IndexError:
- flash("This election does not exist, check"\
- " if you have used the correct URL.")
- raise redirect("/")
- except (IndexError, TypeError):
- flash("This election does not exist, check if"\
- " you have used the correct URL.")
- raise redirect("/")
-
- usernamemap = {}
-
- if election.usefas:
- for c in election.candidates:
- try:
- usernamemap[c.id] = self.fas.person_by_username(
- c.name)['human_name']
- except (AuthError, KeyError) :
- # User has their name set to private
- usernamemap[c.id] = c.name
-
- curtime = datetime.utcnow()
- if election.end_date > curtime:
- turbogears.flash("We are sorry, the results for this"\
- " election cannot be viewed at this time because the"\
- " election is still in progress.")
- raise turbogears.redirect("/")
- elif election.start_date > curtime:
- turbogears.flash("We are sorry, the results for this election cannot be viewed at this time because the election has not started.")
- raise turbogears.redirect("/")
- elif election.embargoed == 1:
- if identity.in_group(config.get('admingroup', 'elections')):
- pass
- else :
- match = 0
- admingroups = model.DBSession.query(ElectionAdmins).filter_by(
- election_id=eid).all()
- for group in admingroups:
- if identity.in_group(group.group_name):
- match = 1
- if match == 0:
- turbogears.flash("Meep, We are sorry, the results"\
- " for this election cannot be viewed because they"\
- " are currently embargoed pending formal"
- " announcement.")
- raise turbogears.redirect("/")
- votecount = model.DBSession.query(Candidates,
- func.sum(Votes.weight).label('novotes'
- )).filter(
- Candidates.id == Votes.candidate_id
- ).filter_by(
- election_id=eid,
- ).order_by(
- func.sum(Votes.weight).desc()
- ).all()
-
-
- return dict(votecount=votecount, usernamemap=usernamemap,
- election=election, appTitle=self.appTitle)
-
- @expose()
- def post_login(self, came_from='/', *args, **kwargs):
- """
- Redirect the user to the initially requested page on successful
- authentication or redirect her back to the login page if login failed.
-
- """
- ## default code from TG2:
- if not request.identity:
- login_counter = request.environ['repoze.who.logins'] + 1
- redirect('/login', came_from=came_from, __logins=login_counter)
- userid = request.identity['repoze.who.userid']
- flash(_('Welcome back, %s!') % userid)
- redirect(came_from)
-
- @expose()
- def post_logout(self, came_from='/'):
- """
- Redirect the user to the initially requested page on logout and say
- goodbye as well.
-
- """
- flash(_('We hope to see you soon!'))
- redirect(came_from)
diff --git a/elections2/controllers/secure.py b/elections2/controllers/secure.py
deleted file mode 100644
index 46d7654..0000000
--- a/elections2/controllers/secure.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Sample controller with all its actions protected."""
-from tg import expose, flash
-from pylons.i18n import ugettext as _, lazy_ugettext as l_
-from repoze.what.predicates import has_permission
-#from dbsprockets.dbmechanic.frameworks.tg2 import DBMechanic
-#from dbsprockets.saprovider import SAProvider
-
-from elections2.lib.base import BaseController
-#from elections2.model import DBSession, metadata
-
-__all__ = ['SecureController']
-
-
-class SecureController(BaseController):
- """Sample controller-wide authorization"""
-
- # The predicate that must be met for all the actions in this controller:
- allow_only = has_permission('manage',
- msg=l_('Only for people with the "manage" permission'))
-
- @expose('elections2.templates.index')
- def index(self):
- """Let the user know that's visiting a protected controller."""
- flash(_("Secure Controller here"))
- return dict(page='index')
-
- @expose('elections2.templates.index')
- def some_where(self):
- """Let the user know that this action is protected too."""
- return dict(page='some_where')
diff --git a/elections2/controllers/template.py b/elections2/controllers/template.py
deleted file mode 100644
index c22a566..0000000
--- a/elections2/controllers/template.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Fallback controller."""
-
-from elections2.lib.base import BaseController
-from tg import abort
-
-__all__ = ['TemplateController']
-
-
-class TemplateController(BaseController):
- """
- The fallback controller for elections2.
-
- By default, the final controller tried to fulfill the request
- when no other routes match. It may be used to display a template
- when all else fails, e.g.::
-
- def view(self, url):
- return render('/%s' % url)
-
- Or if you're using Mako and want to explicitly send a 404 (Not
- Found) response code when the requested template doesn't exist::
-
- import mako.exceptions
-
- def view(self, url):
- try:
- return render('/%s' % url)
- except mako.exceptions.TopLevelLookupException:
- abort(404)
-
- """
-
- def view(self, url):
- """Abort the request with a 404 HTTP status code."""
- abort(404)
diff --git a/elections2/i18n/ru/LC_MESSAGES/elections2.po b/elections2/i18n/ru/LC_MESSAGES/elections2.po
deleted file mode 100644
index 36532d4..0000000
--- a/elections2/i18n/ru/LC_MESSAGES/elections2.po
+++ /dev/null
@@ -1,24 +0,0 @@
-# Russian translations for ${package}.
-# Copyright (C) 2008 ORGANIZATION
-# This file is distributed under the same license as the ${package} project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2008.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: ${package} 0.0.0\n"
-"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2008-01-13 14:00+0200\n"
-"PO-Revision-Date: 2008-01-13 14:00+0200\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: ru <LL(a)li.org>\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
-"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.1\n"
-
-#: ${package}/controllers/root.py:13
-msgid "Your application is now running"
-msgstr "Ваши приложение успешно запущено"
-
diff --git a/elections2/lib/__init__.py b/elections2/lib/__init__.py
deleted file mode 100644
index 633f866..0000000
--- a/elections2/lib/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# -*- coding: utf-8 -*-
-
diff --git a/elections2/lib/admin.py b/elections2/lib/admin.py
deleted file mode 100644
index 126c7ae..0000000
--- a/elections2/lib/admin.py
+++ /dev/null
@@ -1,273 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2008 Nigel Jones, Toshio Kuratomi, Ricky Zhou, Luca Foppiano
-#
-# This copyrighted material is made available to anyone wishing to use, modify,
-# copy, or redistribute it subject to the terms and conditions of the GNU
-# General Public License v.2. This program is distributed in the hope that it
-# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the
-# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-# See the GNU General Public License for more details. You should have
-# received a copy of the GNU General Public License along with this program;
-# if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
-# Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks that are
-# incorporated in the source code or documentation are not subject to the GNU
-# General Public License and may only be used or replicated with the express
-# permission of Red Hat, Inc.
-#
-# Author(s): Nigel Jones <nigelj(a)fedoraproject.org>
-# Toshio Kuratomi <toshio(a)fedoraproject.org>
-# Ricky Zhou <ricky(a)fedoraproject.org>
-# Luca Foppiano <lfoppiano(a)fedoraproject.org>
-#
-# Report Bugs to https://www.fedorahosted.org/elections
-
-
-# Admin functions go here (i.e. add/delete elections)
-
-#import turbogears
-#from turbogears import controllers, expose, flash, redirect, config
-#from turbogears import identity
-from tg import config, expose, flash, require, url, request, redirect
-from repoze.what import predicates
-from elections2.lib.base import BaseController
-from fedora.client import AuthError, AppError
-from elections2 import model
-from elections2.model import Elections, Candidates, \
- LegalVoters, ElectionAdmins
-
-from datetime import datetime
-
-import sqlalchemy, tg
-
-#from turbogears.database import session
-
-request.identity = request.environ.get('repoze.who.identity')
-
-class Admin(BaseController):
- allow_only = predicates.in_group(config.get('admingroup', 'elections'))
-
- def __init__(self, fas, appTitle):
- self.fas = fas
- self.appTitle = appTitle
-
- @expose("json")
- def list_elections(self, **kw):
- electlist = model.DBSession.query(Elections).order_by(
- Elections.start_date).filter('id>0').all()
- elections = [{ 'id': e.id, 'alias': e.alias,
- 'shortdesc': e.shortdesc,
- 'start_date': e.start_date,
- 'end_date': e.end_date,
- 'legal_voters': [
- {'groupname': lv.group_name} for lv in
- model.DBSession.query(LegalVoters
- ).filter_by(election_id=e.id)
- ]} for e in electlist]
- return dict(elections=elections, servertime=datetime.utcnow(),
- appTitle=self.appTitle)
-
- @expose(template='elections2.templates.admin')
- def index(self, *kw, **args):
- electlist = model.DBSession.query(Elections).order_by(
- Elections.start_date).filter('id>0').all()
- elections = [{'id': e.id, 'alias': e.alias,
- 'shortdesc': e.shortdesc,
- 'start_date': e.start_date,
- 'end_date': e.end_date,
- 'legal_voters': [
- {'groupname': lv.group_name} for lv in
- model.DBSession.query(LegalVoters
- ).filter_by(election_id=e.id)],
- 'embargoed' : e.embargoed}
- for e in electlist
- ]
- print "here"
- return dict(elections=elections)
-
- @expose(template="elections2.templates.admnewe")
- def newe(self, **kw):
- print "newe"
- if "submit" in kw:
- print kw
- setembargo=1
- usefas=1
- if "embargoed" not in kw:
- setembargo=0
- if "usefas" not in kw:
- usefas=0
- if kw["seats"] == "":
- flash("The number of seats given is incorrect.")
- return dict()
-
- election = Elections()
- election.alias=kw['alias'] #status=0, method=0,
- election.shortdesc=kw['shortdesc']
- election.description=kw['info']
- election.url=kw['url']
- election.start_date= datetime.strptime(kw['startdate'],
- "%Y-%m-%d %H:%M:%S")
- election.end_date=datetime.strptime(kw['enddate'],
- "%Y-%m-%d %H:%M:%S")
- election.embargoed=setembargo
- election.seats_elected=kw['seats']
- election.usefas=usefas
- election.votes_per_user=1
- flash("New Election Created")
- print dir(model.DBSession)
- model.DBSession.add(election)
- #model.DBSession.commit()
- print "raise"
- raise redirect("/admin/edit/"+str(election.alias))
- else:
- print "return"
- return dict()
-
- @expose(template="elections2.templates.admnewc")
- def newc(self, **kw):
- if "submit" in kw:
- for entry in kw['nameurl'].split("|"):
- candidate = entry.split("!")
- #Python doesn't have a good way of doing case/switch statements
- if len(candidate) == 1:
- Candidates(election_id=kw['id'], name=candidate[0],
- status=0, human=1)
- elif len(candidate) == 2:
- Candidates(election_id=kw['id'], name=candidate[0],
- url=candidate[1], status=0, human=1)
- else:
- flash("There was an issue!")
- raise redirect("/admin/newc")
- else:
- return dict()
-
- @expose(template="elections2.templates.admedit")
- def edit(self, eid=None, **kw):
- print "**", eid, kw
- if "submit" in kw:
- for entry in kw['newgroups'].split("|"):
- entry.strip()
- if len(entry) :
- lv = LegalVoters()
- lv.election_id=kw['id']
- lv.group_name=entry
- model.DBSession.add(lv)
- for entry in kw['newadmins'].split("|"):
- entry.strip()
- if len(entry) :
- ea = ElectionAdmins()
- ea.election_id=kw['id']
- ea.group_name=entry
- model.DBSession.add(ea)
- for key, value in kw.items():
- if key.startswith('remove_'):
- group = key[len('remove_'):]
- for lv in model.DBSession.query(LegalVoters).filter_by(
- election_id=kw['id'],group_name=group) :
- model.DBSession.delete(lv)
- for key, value in kw.items():
- if key.startswith('removeadmin_'):
- group = key[len('removeadmin_'):]
- for admin in model.DBSession.query(ElectionAdmins).filter_by(
- election_id=kw['id'],group_name=group) :
- model.DBSession.delete(admin)
- for entry in kw['newcandidates'].split("|"):
- candidate = entry.split("!")
- #Python doesn't have a good way of doing case/switch statements
- if len(candidate) == 1:
- if len(candidate[0]):
- c=Candidates()
- c.election_id=kw['id']
- c.name=candidate[0]
- c.status=0
- c.human=1
- model.DBSession.add(c)
- elif len(candidate) == 2:
- if len(candidate[0]):
- c = Candidates()
- c.election_id=kw['id']
- c.name=candidate[0]
- c.url=candidate[1]
- c.status=0
- c.human=1
- model.DBSession.add(c)
- else:
- flash("There was an issue!")
- setembargo=1
- usefas=1
- nominations=1
- if "embargoed" not in kw:
- setembargo=0
- if "usefas" not in kw:
- usefas=0
- if "allownominations" not in kw:
- nominations=0
- try:
- election = model.DBSession.query(Elections).filter_by(
- id=int(eid)).all()[0]
- except ValueError:
- election = model.DBSession.query(Elections).filter_by(
- alias=eid).all()[0]
- print kw.keys()
- election.alias=kw['alias']
- election.status=0
- election.method=0
- election.shortdesc=kw['shortdesc']
- election.description=kw['info']
- election.url=kw['url']
- election.start_date=election.end_date=datetime.strptime(kw['startdate'],
- "%Y-%m-%d %H:%M:%S")
- election.end_date=election.end_date=datetime.strptime(kw['enddate'],
- "%Y-%m-%d %H:%M:%S")
- election.embargoed=setembargo
- election.seats_elected=kw['seats']
- election.usefas=usefas
- election.votes_per_user=kw['votes']
- election.allow_nominations=nominations
- model.DBSession.add(election)
- raise redirect("/admin/edit/"+kw['id'])
-
- try:
- eid = int(eid)
- election = election = model.DBSession.query(Elections
- ).filter_by(id=eid).all()[0]
- except ValueError:
- try:
- election = election = model.DBSession.query(Elections
- ).filter_by(alias=eid).all()[0]
- eid = election.id
- except IndexError:
- flash("This election does not exist, check if you have used the correct URL.")
- raise redirect("/admin/")
- except (IndexError, TypeError):
- flash("This election does not exist, check if you have used the correct URL.")
- raise redirect("/admin/")
-
- if "removeembargo" in kw:
- election.embargoed=0
- flash("Embargo on election results removed")
- raise redirect("/admin/")
-
- candidates = model.DBSession.query(Candidates
- ).filter_by(election_id=election.id).all()
- votergroups = model.DBSession.query(LegalVoters
- ).filter_by(election_id=election.id).all()
- admingroups = model.DBSession.query(ElectionAdmins
- ).filter_by(election_id=election.id).all()
- groupnamemap = {}
- for g in admingroups:
- try:
- groupnamemap[g.group_name] = g.group_name + " ("
- + self.fas.group_by_name(g.group_name)['display_name'] +")"
- except (AppError, AuthError, KeyError) :
- groupnamemap[g.group_name] = g.group_name
- for g in votergroups:
- try:
- groupnamemap[g.group_name] = g.group_name + " ("
- + self.fas.group_by_name(g.group_name)['display_name'] +")"
- except (AppError, AuthError, KeyError) :
- groupnamemap[g.group_name] = g.group_name
-
- return dict(e=election, candidates=candidates,
- admingroups=admingroups, groups=votergroups,
- groupnamemap=groupnamemap)
diff --git a/elections2/lib/app_globals.py b/elections2/lib/app_globals.py
deleted file mode 100644
index 0bd3089..0000000
--- a/elections2/lib/app_globals.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""The application's Globals object"""
-
-__all__ = ['Globals']
-
-
-class Globals(object):
- """Container for objects available throughout the life of the application.
-
- One instance of Globals is created during application initialization and
- is available during requests via the 'app_globals' variable.
-
- """
-
- def __init__(self):
- """Do nothing, by default."""
- pass
diff --git a/elections2/lib/base.py b/elections2/lib/base.py
deleted file mode 100644
index 8ddcf23..0000000
--- a/elections2/lib/base.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""The base Controller API."""
-
-from tg import TGController, tmpl_context
-from tg.render import render
-from tg import request
-from pylons.i18n import _, ungettext, N_
-from tw.api import WidgetBunch
-import elections2.model as model
-
-__all__ = ['BaseController']
-
-
-class BaseController(TGController):
- """
- Base class for the controllers in the application.
-
- Your web application should have one of these. The root of
- your application is used to compute URLs used by your app.
-
- """
-
- def __call__(self, environ, start_response):
- """Invoke the Controller"""
- # TGController.__call__ dispatches to the Controller method
- # the request is routed to. This routing information is
- # available in environ['pylons.routes_dict']
-
- request.identity = request.environ.get('repoze.who.identity')
- tmpl_context.identity = request.identity
- return TGController.__call__(self, environ, start_response)
diff --git a/elections2/lib/helpers.py b/elections2/lib/helpers.py
deleted file mode 100644
index 8f35133..0000000
--- a/elections2/lib/helpers.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""WebHelpers used in elections2."""
-
-from webhelpers import date, feedgenerator, html, number, misc, text
diff --git a/elections2/lib/vote.py b/elections2/lib/vote.py
deleted file mode 100644
index 81e8ccf..0000000
--- a/elections2/lib/vote.py
+++ /dev/null
@@ -1,198 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2008 Nigel Jones, Toshio Kuratomi, Ricky Zhou, Luca Foppiano
-#
-# This copyrighted material is made available to anyone wishing to use, modify,
-# copy, or redistribute it subject to the terms and conditions of the GNU
-# General Public License v.2. This program is distributed in the hope that it
-# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the
-# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-# See the GNU General Public License for more details. You should have
-# received a copy of the GNU General Public License along with this program;
-# if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
-# Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks that are
-# incorporated in the source code or documentation are not subject to the GNU
-# General Public License and may only be used or replicated with the express
-# permission of Red Hat, Inc.
-#
-# Author(s): Nigel Jones <nigelj(a)fedoraproject.org>
-# Toshio Kuratomi <toshio(a)fedoraproject.org>
-# Ricky Zhou <ricky(a)fedoraproject.org>
-# Luca Foppiano <lfoppiano(a)fedoraproject.org>
-#
-# Report Bugs to https://www.fedorahosted.org/elections
-
-
-# Voting functions
-
-#import turbogears
-#from turbogears import controllers, expose, flash, redirect, config
-#from turbogears import identity
-from tg import expose, flash, require, url, request, redirect
-from repoze.what import predicates
-from elections2.lib.base import BaseController
-
-from elections2 import model
-from elections2.model import Elections, LegalVoters, Candidates, \
- Votes
-
-import sqlalchemy
-from sqlalchemy import func
-
-#from turbogears.database import session
-
-from datetime import datetime
-import re
-
-class Vote(BaseController):
- allow_only = predicates.not_anonymous()
-
- def __init__(self, fas, appTitle):
- self.fas = fas
- self.appTitle = appTitle
-
- #TODO: This function will be split off into: default => submit =>
- #confirm functions, hopefully it was simplify everything
- @expose(template="elections2.templates.vote")
- def default(self, eid=None, **kw):
- print dir(Elections)
- try:
- eid = int(eid)
- election = model.DBSession.query(Elections
- ).filter_by(id=eid).all()[0]
- except ValueError:
- try:
- election = model.DBSession.query(Elections
- ).filter_by(alias=eid).all()[0]
- eid = election.id
- except IndexError:
- flash("This election does not exist, check"\
- " if you have used the correct URL.")
- raise redirect("/")
- except IndexError:
- flash("This election does not exist, check if"\
- " you have used the correct URL.")
- raise redirect("/")
-
- request.identity = request.environ.get('repoze.who.identity')
- votergroups = model.DBSession.query(LegalVoters
- ).filter_by(election_id=eid).all()
- usergroups = request.identity["groups"]
- print usergroups
- print votergroups
-
- match = 0
- for group in votergroups:
- if group.group_name == "anycla":
- if (len(usergroups) > len([g for g in
- usergroups if re.match("cla_.*",g)])
- ):
- match = 1
- elif predicates.in_group(group.group_name) or \
- group.group_name == "anyany":
- match = 1
- if match == 0:
- flash("You are not in a FAS group that can vote"\
- " in this election, more information can be found at"\
- " the bottom of this page.")
- raise redirect("/about/" + str(eid))
-
- candidates = model.DBSession.query(Candidates
- ).filter_by(election_id=eid
- ).order_by(Candidates.name).all()
- uservote = model.DBSession.query(Votes.election_id,
- Votes.voter, func.count(Votes.voter)
- ).group_by(Votes.voter).all()
-
- usernamemap = {}
-
- if election.usefas:
- for c in candidates:
- try:
- usernamemap[c.id] = self.fas.person_by_username(
- c.name)['human_name']
- except KeyError:
- # User has human_name set to private
- usernamemap[c.id] = c.name
-
- uvotes = {}
- next_action = ""
-
- curtime = datetime.utcnow()
- if election.start_date > curtime:
- flash("Voting has not yet started, sorry.")
- raise redirect("/")
- elif election.end_date < curtime:
- flash("You cannot vote in this election because the end date has passed. You have been redirected to the election results")
- raise redirect("/results/" + election.shortname)
- elif len(uservote) != 0:
- flash("You have already voted in this election!")
- raise redirect("/")
-
- # Lets do this in reverse order
- if "confirm" in kw:
- for c in candidates:
- if str(c.id) in kw:
- try:
- range = int(kw[str(c.id)])
- if range >= 0 and range <= len(candidates):
- uvotes[c.id] = range
- else:
- flash("Invalid Ballot!")
- raise redirect("/")
- except ValueError:
- flash("Invalid Ballot!")
- raise redirect("/")
- for uvote in uvotes:
- v = Votes()
- v.voter=request.identity['username']
- v.candidate_id=uvote
- v.weight=uvotes[uvote]
- v.election_id=eid
- v.timestamp=curtime
- model.DBSession.add(v)
- flash("Your vote has been recorded, thank you!")
- raise redirect("/")
- elif "vote" in kw:
- flash("Please confirm your vote!")
- for c in candidates:
- if str(c.id) in kw:
- try:
- range = int(kw[str(c.id)])
- if range > len(candidates):
- flash("Invalid data was detected for one " \
- "or more candidates and was changed to " \
- "zeros! Please correct and resubmit your "\
- "ballot.")
- uvotes[c.id] = 0
- next_action = "vote"
- elif range >= 0:
- uvotes[c.id] = range
- else:
- flash("Invalid data was " \
- "detected for one or more candidates and" \
- "was changed to zeros! Please correct " \
- "and resubmit your ballot.")
- uvotes[c.id] = 0
- next_action = "vote"
- except ValueError:
- flash("Invalid data was detected for one or " \
- "more candidates and was changed to zeros! " \
- "Please correct and resubmit your ballot.")
- uvotes[c.id] = 0
- next_action = "vote"
- else:
- flash("Invalid Ballot!")
- raise redirect("/")
- if next_action != "vote":
- next_action = "confirm"
- else:
- for c in candidates:
- uvotes[c.id] = ""
- next_action = "vote"
-
- return dict(eid=eid, candidates=candidates,
- usernamemap=usernamemap, election=election,
- nextaction=next_action, voteinfo=uvotes,
- appTitle=self.appTitle)
-
diff --git a/elections2/model/__init__.py b/elections2/model/__init__.py
deleted file mode 100644
index 8c7e2db..0000000
--- a/elections2/model/__init__.py
+++ /dev/null
@@ -1,189 +0,0 @@
-# -*- coding: utf-8 -*-
-"""The application's model objects"""
-
-from zope.sqlalchemy import ZopeTransactionExtension
-from sqlalchemy.orm import scoped_session, sessionmaker, mapper, relation
-from sqlalchemy import Table, ForeignKey, Column
-from sqlalchemy.types import Unicode, Integer, DateTime, String
-#from sqlalchemy import MetaData
-from sqlalchemy.ext.declarative import declarative_base
-
-# Global session manager: DBSession() returns the Thread-local
-# session object appropriate for the current web request.
-maker = sessionmaker(autoflush=True, autocommit=False,
- extension=ZopeTransactionExtension())
-DBSession = scoped_session(maker)
-
-# Base class for all of our model classes: By default, the data model is
-# defined with SQLAlchemy's declarative extension, but if you need more
-# control, you can switch to the traditional method.
-DeclarativeBase = declarative_base()
-
-# There are two convenient ways for you to spare some typing.
-# You can have a query property on all your model classes by doing this:
-# DeclarativeBase.query = DBSession.query_property()
-# Or you can use a session-aware mapper as it was used in TurboGears 1:
-# DeclarativeBase = declarative_base(mapper=DBSession.mapper)
-
-# Global metadata.
-# The default metadata is the one from the declarative base.
-metadata = DeclarativeBase.metadata
-
-# If you have multiple databases with overlapping table names, you'll need a
-# metadata for each database. Feel free to rename 'metadata2'.
-#metadata2 = MetaData()
-
-#####
-# Generally you will not want to define your table's mappers, and data objects
-# here in __init__ but will want to create modules them in the model directory
-# and import them at the bottom of this file.
-#
-######
-
-def init_model(engine):
- """Call me before using any of the tables or classes in the model."""
-
- DBSession.configure(bind=engine)
- metadata.bind = engine
- # If you are using reflection to introspect your database and create
- # table objects for you, your tables must be defined and mapped inside
- # the init_model function, so that the engine is available if you
- # use the model outside tg2, you need to make sure this is called before
- # you use the model.
-
- # View in the DB. Needs to have the column keys defined
-
-
-# Import your model modules here.
-from elections2.model.auth import User, Group, Permission
-
-#
-# Classes to map to
-#
-
-
-class LegalVoters(DeclarativeBase):
- """
- Define the voters.
- """
- __tablename__ = 'legalvoters'
-
- #{ Columns
- id = Column(Integer, autoincrement=True, primary_key=True, unique=True)
- election_id = Column(Integer, ForeignKey('elections.id'), nullable=False)
- group_name = Column(Unicode(150), nullable=False)
-
- #{ Special methods
- def __repr__(self):
- return '<LegalVoters: group=%r>' % self.group_name
-
- def __unicode__(self):
- return self.group_name
- #}
-
-
-class ElectionAdmins(DeclarativeBase):
- """
- Define the Admins.
- """
- __tablename__ = 'electionadmins'
-
- #{ Columns
- id = Column(Integer, autoincrement=True, primary_key=True, unique=True)
- election_id = Column(Integer, ForeignKey('elections.id'), nullable=False)
- group_name = Column(Unicode(150), nullable=False)
-
- #{ Special methods
- def __repr__(self):
- return '<ElectionAdmins: group=%r>' % self.group_name
-
- def __unicode__(self):
- return self.group_name
- #}
-
-
-class Elections(DeclarativeBase):
- """
- Define the different elections.
- """
- __tablename__ = 'elections'
-
- #{ Columns
- id = Column(Integer, autoincrement=True, primary_key=True)
- shortdesc = Column(Unicode(150), unique=True, nullable=False)
- alias = Column(Unicode(150), unique=True, nullable=False)
- description = Column(Unicode(150), unique=True, nullable=False)
- url = Column(Unicode(150), unique=True, nullable=False)
- start_date = Column(DateTime, nullable=False)
- end_date = Column(DateTime, nullable=False)
- seats_elected = Column(Integer, nullable=False)
- votes_per_user = Column(Integer, nullable=False)
- embargoed = Column(Integer, nullable=False, default=0)
- usefas = Column(Integer, nullable=False, default=0)
- allow_nominations = Column(Integer, nullable=False, default=0)
- nominations_until = Column(DateTime)
-
- #{ Relations
- legalVoters = relation('LegalVoters')
- candidates = relation('Candidates')
- #uservotes = relation('UserVoteCount')
-
- #{ Special methods
- def __repr__(self):
- return '<Elections: election=%r>' % self.shortdesc
-
- def __unicode__(self):
- return self.shortdesc
- #}
-
-
-class Votes(DeclarativeBase):
- """
- Define the different votes.
- """
- __tablename__ = 'votes'
-
- #{ Columns
- id = Column(Integer, autoincrement=True, primary_key=True, unique=True)
- election_id = Column(Integer, ForeignKey('elections.id'), nullable=False)
- voter = Column(Unicode(150), nullable=False)
- timestamp = Column(DateTime, nullable=False)
- candidate_id = Column(Integer, ForeignKey('candidates.id'), nullable=False)
- weight = Column(Integer, nullable=False)
-
- #{ Special methods
- def __repr__(self):
- return '<Votes: voter=%r>' % self.voter
-
- def __unicode__(self):
- return self.voter
- #}
-
-
-class Candidates(DeclarativeBase):
- """
- Define the candidates.
- """
- __tablename__ = 'candidates'
-
- #{ Columns
- id = Column(Integer, autoincrement=True, primary_key=True)
- election_id = Column(Integer, ForeignKey('elections.id'),
- primary_key=True, nullable=False)
- name = Column(Unicode(150), nullable=False)
- url = Column(Unicode(150))
- formalname = Column(Unicode(150), nullable=True)
- human = Column(Integer)
- status = Column(Integer)
-
- #{ Relations
- votes = relation('Votes', backref='candidate'),
- #tally = relation('VoteTally', backref='candidate')
-
- #{ Special methods
- def __repr__(self):
- return '<Candidates: candidate=%r>' % self.name
-
- def __unicode__(self):
- return self.name
- #}
diff --git a/elections2/model/auth.py b/elections2/model/auth.py
deleted file mode 100644
index 65a8cbd..0000000
--- a/elections2/model/auth.py
+++ /dev/null
@@ -1,227 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Auth* related model.
-
-This is where the models used by :mod:`repoze.who` and :mod:`repoze.what` are
-defined.
-
-It's perfectly fine to re-use this definition in the elections2 application,
-though.
-
-"""
-import os
-from datetime import datetime
-import sys
-try:
- from hashlib import sha1
-except ImportError:
- sys.exit('ImportError: No module named hashlib\n'
- 'If you are on python2.4 this library is not part of python. '
- 'Please install it. Example: easy_install hashlib')
-
-from sqlalchemy import Table, ForeignKey, Column
-from sqlalchemy.types import Unicode, Integer, DateTime
-from sqlalchemy.orm import relation, synonym
-
-from elections2.model import DeclarativeBase, metadata, DBSession
-
-__all__ = ['User', 'Group', 'Permission']
-
-
-#{ Association tables
-
-
-# This is the association table for the many-to-many relationship between
-# groups and permissions. This is required by repoze.what.
-group_permission_table = Table('tg_group_permission', metadata,
- Column('group_id', Integer, ForeignKey('tg_group.group_id',
- onupdate="CASCADE", ondelete="CASCADE"), primary_key=True),
- Column('permission_id', Integer, ForeignKey('tg_permission.permission_id',
- onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
-)
-
-# This is the association table for the many-to-many relationship between
-# groups and members - this is, the memberships. It's required by repoze.what.
-user_group_table = Table('tg_user_group', metadata,
- Column('user_id', Integer, ForeignKey('tg_user.user_id',
- onupdate="CASCADE", ondelete="CASCADE"), primary_key=True),
- Column('group_id', Integer, ForeignKey('tg_group.group_id',
- onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
-)
-
-
-#{ The auth* model itself
-
-
-class Group(DeclarativeBase):
- """
- Group definition for :mod:`repoze.what`.
-
- Only the ``group_name`` column is required by :mod:`repoze.what`.
-
- """
-
- __tablename__ = 'tg_group'
-
- #{ Columns
-
- group_id = Column(Integer, autoincrement=True, primary_key=True)
-
- group_name = Column(Unicode(16), unique=True, nullable=False)
-
- display_name = Column(Unicode(255))
-
- created = Column(DateTime, default=datetime.now)
-
- #{ Relations
-
- users = relation('User', secondary=user_group_table, backref='groups')
-
- #{ Special methods
-
- def __repr__(self):
- return '<Group: name=%r>' % self.group_name
-
- def __unicode__(self):
- return self.group_name
-
- #}
-
-
-# The 'info' argument we're passing to the email_address and password columns
-# contain metadata that Rum (http://python-rum.org/) can use generate an
-# admin interface for your models.
-class User(DeclarativeBase):
- """
- User definition.
-
- This is the user definition used by :mod:`repoze.who`, which requires at
- least the ``user_name`` column.
-
- """
- __tablename__ = 'tg_user'
-
- #{ Columns
-
- user_id = Column(Integer, autoincrement=True, primary_key=True)
-
- user_name = Column(Unicode(16), unique=True, nullable=False)
-
- email_address = Column(Unicode(255), unique=True, nullable=False,
- info={'rum': {'field':'Email'}})
-
- display_name = Column(Unicode(255))
-
- _password = Column('password', Unicode(80),
- info={'rum': {'field':'Password'}})
-
- created = Column(DateTime, default=datetime.now)
-
- #{ Special methods
-
- def __repr__(self):
- return '<User: name=%r, email=%r, display=%r>' % (
- self.user_name, self.email_address, self.display_name)
-
- def __unicode__(self):
- return self.display_name or self.user_name
-
- #{ Getters and setters
-
- @property
- def permissions(self):
- """Return a set with all permissions granted to the user."""
- perms = set()
- for g in self.groups:
- perms = perms | set(g.permissions)
- return perms
-
- @classmethod
- def by_email_address(cls, email):
- """Return the user object whose email address is ``email``."""
- return DBSession.query(cls).filter(cls.email_address == email).first()
-
- @classmethod
- def by_user_name(cls, username):
- """Return the user object whose user name is ``username``."""
- return DBSession.query(cls).filter(cls.user_name == username).first()
-
- def _set_password(self, password):
- """Hash ``password`` on the fly and store its hashed version."""
- # Make sure password is a str because we cannot hash unicode objects
- if isinstance(password, unicode):
- password = password.encode('utf-8')
- salt = sha1()
- salt.update(os.urandom(60))
- hash = sha1()
- hash.update(password + salt.hexdigest())
- password = salt.hexdigest() + hash.hexdigest()
- # Make sure the hashed password is a unicode object at the end of the
- # process because SQLAlchemy _wants_ unicode objects for Unicode cols
- if not isinstance(password, unicode):
- password = password.decode('utf-8')
- self._password = password
-
- def _get_password(self):
- """Return the hashed version of the password."""
- return self._password
-
- password = synonym('_password', descriptor=property(_get_password,
- _set_password))
-
- #}
-
- def validate_password(self, password):
- """
- Check the password against existing credentials.
-
- :param password: the password that was provided by the user to
- try and authenticate. This is the clear text version that we will
- need to match against the hashed one in the database.
- :type password: unicode object.
- :return: Whether the password is valid.
- :rtype: bool
-
- """
- hash = sha1()
- if isinstance(password, unicode):
- password = password.encode('utf-8')
- hash.update(password + str(self.password[:40]))
- return self.password[40:] == hash.hexdigest()
-
-
-class Permission(DeclarativeBase):
- """
- Permission definition for :mod:`repoze.what`.
-
- Only the ``permission_name`` column is required by :mod:`repoze.what`.
-
- """
-
- __tablename__ = 'tg_permission'
-
- #{ Columns
-
- permission_id = Column(Integer, autoincrement=True, primary_key=True)
-
- permission_name = Column(Unicode(63), unique=True, nullable=False)
-
- description = Column(Unicode(255))
-
- #{ Relations
-
- groups = relation(Group, secondary=group_permission_table,
- backref='permissions')
-
- #{ Special methods
-
- def __repr__(self):
- return '<Permission: name=%r>' % self.permission_name
-
- def __unicode__(self):
- return self.permission_name
-
- #}
-
-
-#}
diff --git a/elections2/model/model.template b/elections2/model/model.template
deleted file mode 100644
index c1eebc4..0000000
--- a/elections2/model/model.template
+++ /dev/null
@@ -1,22 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Sample model module."""
-
-from sqlalchemy import *
-from sqlalchemy.orm import mapper, relation
-from sqlalchemy import Table, ForeignKey, Column
-from sqlalchemy.types import Integer, Unicode
-#from sqlalchemy.orm import relation, backref
-
-from elections2.model import DeclarativeBase, metadata, DBSession
-
-
-class SampleModel(DeclarativeBase):
- __tablename__ = 'sample_model'
-
- #{ Columns
-
- id = Column(Integer, primary_key=True)
-
- data = Column(Unicode(255), nullable=False)
-
- #}
diff --git a/elections2/public/css/style.css b/elections2/public/css/style.css
deleted file mode 100644
index fe03cd9..0000000
--- a/elections2/public/css/style.css
+++ /dev/null
@@ -1,57 +0,0 @@
-.flash
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #EEFFBB;
-}
-
-.vthot
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #FFFFFF;
- background: #9C0000;
-}
-.vtwarm
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #FFA300;
-}
-.vtmedwarm
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #93FF63;
-}
-.vtmedcool
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #00DAFE;
-}
-.vtcool
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #000000;
- background: #0033FF;
-}
-.vtcold
-{
- border: 1px solid #DDDDDD;
- padding: 1.5ex;
- margin: 1ex 0;
- color: #FFFFFF;
- background: #000083;
-}
diff --git a/elections2/public/favicon.ico b/elections2/public/favicon.ico
deleted file mode 100644
index 840986e..0000000
Binary files a/elections2/public/favicon.ico and /dev/null differ
diff --git a/elections2/public/images/favicon.ico b/elections2/public/images/favicon.ico
deleted file mode 100644
index 332557b..0000000
Binary files a/elections2/public/images/favicon.ico and /dev/null differ
diff --git a/elections2/public/images/header_inner.png b/elections2/public/images/header_inner.png
deleted file mode 100644
index 2b2d87d..0000000
Binary files a/elections2/public/images/header_inner.png and /dev/null differ
diff --git a/elections2/public/images/info.png b/elections2/public/images/info.png
deleted file mode 100644
index 329c523..0000000
Binary files a/elections2/public/images/info.png and /dev/null differ
diff --git a/elections2/public/images/ok.png b/elections2/public/images/ok.png
deleted file mode 100644
index fee6751..0000000
Binary files a/elections2/public/images/ok.png and /dev/null differ
diff --git a/elections2/public/images/tg_under_the_hood.png b/elections2/public/images/tg_under_the_hood.png
deleted file mode 100644
index bc9c79c..0000000
Binary files a/elections2/public/images/tg_under_the_hood.png and /dev/null differ
diff --git a/elections2/public/images/under_the_hood_blue.png b/elections2/public/images/under_the_hood_blue.png
deleted file mode 100644
index 90e84b7..0000000
Binary files a/elections2/public/images/under_the_hood_blue.png and /dev/null differ
diff --git a/elections2/templates/__init__.py b/elections2/templates/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/elections2/templates/about.html b/elections2/templates/about.html
deleted file mode 100644
index 4994709..0000000
--- a/elections2/templates/about.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude">
- <xi:include href="master.html" />
- <head>
- <title>Fedora Elections -- Election Information</title>
- </head>
- <body>
- <h1>${election.shortdesc}</h1>
- <p>${election.description}</p>
- <p py:if="election.url"><a href="${election.url}">[More Information]</a></p>
- <table border="1" cellpadding="1">
- <tr py:for="candidate in candidates">
- <td><py:choose test="election.usefas"><py:when test="0">${candidate.name}</py:when><py:when test="1">${usernamemap[candidate.id]}</py:when></py:choose> <small py:if="candidate.url"><a href="${candidate.url}">[info]</a></small></td>
- </tr>
- <py:choose test="curtime > election.end_date"><py:when test="0">
- <py:choose test="election.start_date > curtime"><py:when test="0">
- <tr>
- <td>
- <py:choose test="1">
- <py:when test="election.alias == ''">
- <a href="${tg.url('/vote/' + str(election.id))}">Vote Now!</a>
- </py:when>
- <py:otherwise>
- <a href="${tg.url('/vote/' + str(election.alias))}">Vote Now!</a>
- </py:otherwise>
- </py:choose>
- </td>
- </tr>
- </py:when></py:choose>
- </py:when><py:when test="1">
- <tr>
- <py:choose test="1">
- <py:when test="election.alias == ''">
- <a href="${tg.url('/results/' + str(election.id))}">Show Results!</a>
- </py:when>
- <py:otherwise>
- <a href="${tg.url('/results/' + str(election.alias))}">Show Results!</a>
- </py:otherwise>
- </py:choose>
- </tr>
- </py:when></py:choose>
- </table>
- <p>To vote in this election you must be a member of any one of the following groups:</p>
- <ul>
- <li py:for="g in votergroups"><py:choose test="g.group_name == 'anyany'"><py:when test="0"><py:choose test="g.group_name == 'anycla'"><py:when test="0">${groupnamemap[g.group_name]}</py:when><py:when test="1">Any non-CLA Group</py:when></py:choose></py:when><py:when test="1">Any Group</py:when></py:choose></li>
- </ul>
- </body>
-</html>
diff --git a/elections2/templates/admedit.html b/elections2/templates/admedit.html
deleted file mode 100644
index 3e179a8..0000000
--- a/elections2/templates/admedit.html
+++ /dev/null
@@ -1,74 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude">
- <xi:include href="master.html" />
- <head>
- <title>Fedora Elections -- Administration -- Election Details</title>
- </head>
- <body>
- <form action="${tg.url('/admin/edit/' + str(e.id))}" method="post">
- <table>
- <tr>
- <th colspan="2"> Election Details - ${e.shortdesc} </th>
- </tr>
- <tr><td>Election ID:</td><td>${e.id}<input type="hidden" name="id" value="${e.id}" /></td></tr>
- <tr><td>Election Name:</td><td><input type="text" name="shortdesc" value="${e.shortdesc}" /></td></tr>
- <tr><td>Alias:</td><td><input type="text" name="alias" value="${e.alias}" /></td></tr>
- <tr><td>URL:</td><td><input type="text" name="url" value="${e.url}" /></td></tr>
- <tr><td>Election Info:</td><td><textarea name="info" rows="5" cols="40">${e.description}</textarea></td></tr>
- <tr><td>Start Date:</td><td><input type="text" name="startdate" value="${e.start_date}" /></td></tr>
- <tr><td>End Date:</td><td><input type="text" name="enddate" value="${e.end_date}" /></td></tr>
- <tr><td>Seats Elected:</td><td><input type="text" name="seats" value="${e.seats_elected}" /></td></tr>
- <tr><td>Votes Per User:</td><td><input type="text" name="votes" value="${e.votes_per_user}" /></td></tr>
-<!--
- <tr><td>Allow Nominations?</td><td><input type="checkbox" name="allownominations" py:attrs="{'checked':tg.checker(e.allow_nominations)}" /> </td></tr>
--->
- <tr><td>Until When?</td><td><input type="text" name="nominationend" value="" /> </td></tr>
-<!--
- <tr><td>Use FAS?</td><td><input type="checkbox" name="usefas" py:attrs="{'checked':tg.checker(e.usefas)}" /> </td></tr>
--->
-<!--
- <tr><td>Embargoed?</td><td><input type="checkbox" name="embargoed" py:attrs="{'checked':tg.checker(e.embargoed)}" /> </td></tr>
--->
- <tr><td></td><td><input type="submit" name="submit" value="Submit"/></td></tr>
-
- <tr>
- <th colspan="2"> Allowed Groups </th>
- </tr>
- <tr py:for="g in groups">
- <td>
- <py:choose test="g.group_name == 'anyany'"><py:when test="0"><py:choose test="g.group_name == 'anycla'"><py:when test="0">${groupnamemap[g.group_name]}</py:when><py:when test="1">Any non-CLA Group</py:when></py:choose></py:when><py:when test="1">Any Group</py:when></py:choose>
- </td>
- <td>Remove group: <input type="checkbox" name="remove_${g.group_name}" /> </td>
- </tr>
- <tr><td>New Groups:<br/>( entries seperated by | )</td><td><input type="text" name="newgroups" value="" /> </td></tr>
- <tr><td></td><td><input type="submit" name="submit" value="Submit"/></td></tr>
-
- <tr>
- <th colspan="2"> Admin Groups </th>
- </tr>
- <tr py:for="g in admingroups">
- <td> ${groupnamemap[g.group_name]} </td>
- <td>Remove group: <input type="checkbox" name="removeadmin_${g.group_name}" /> </td>
- </tr>
- <tr><td>New Groups:<br/>( entries seperated by | )</td><td><input type="text" name="newadmins" value="" /> </td></tr>
- <tr><td></td><td><input type="submit" name="submit" value="Submit"/></td></tr>
-
- <tr>
- <th colspan="2"> Candidates </th>
- </tr>
- <tr py:for="c in candidates">
- <td>
- ${c.name}
- </td>
- <td>
- ${c.url}
- </td>
- </tr>
- <tr><td>New Candidates:<br/>( Formated as [URL optional]: Name1!URL1|Name2!URL2 )</td><td><input type="text" name="newcandidates" value="" /> </td></tr>
- <tr><td></td><td><input type="submit" name="submit" value="Submit"/></td></tr>
- </table>
- </form>
- </body>
-</html>
diff --git a/elections2/templates/admin.html b/elections2/templates/admin.html
deleted file mode 100644
index 1bff42e..0000000
--- a/elections2/templates/admin.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude">
- <xi:include href="master.html" />
- <head>
- <title>Fedora Elections -- Administration</title>
- </head>
- <body>
- <table>
- <tr>
- <th>ID</th>
- <th>Name</th>
- <th>Alias</th>
- <th>Start Date</th>
- <th>End Date</th>
- <th></th>
- </tr>
- <tr py:for="election in elections">
- <td>${election.id}</td>
- <td>${election.shortdesc}</td>
- <td>${election.alias}</td>
- <td>${election.start_date}</td>
- <td>${election.end_date}</td>
- <td>
- <a href="${tg.url('/admin/edit/' + str(election.id))}">More Information</a>
- <py:if test="election.embargoed">
- |
- <a href="${tg.url('/admin/edit/' + str(election.id) + '?removeembargo=1')}">Remove Embargo</a>
- </py:if>
- </td>
- </tr>
- </table>
- <p><a href="${tg.url('/admin/newe/')}">Create a new election</a></p>
-
- </body>
-</html>
diff --git a/elections2/templates/admnewc.html b/elections2/templates/admnewc.html
deleted file mode 100644
index a1393f6..0000000
--- a/elections2/templates/admnewc.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude">
- <xi:include href="master.html" />
- <head>
- <title>Fedora Elections -- Administration -- New Candidates</title>
- </head>
- <body>
- <form action="${tg.url('/admin/newc')}" method="post">
- <table>
- <tr><td>Election Id (Numerical):</td><td><input type="text" name="id" /></td></tr>
- <tr><td>The string below needs to be formatted as: "Name1!URL1 (opt)|Name2!URL2 (opt)|Name3!URL3 (opt)"</td></tr>
- <tr><td>Candidate Name/URL String:</td><td><input type="text" name="nameurl" /></td></tr>
- <tr><td></td><td><input type="submit" name="submit" value="Submit" /></td></tr>
- </table>
- </form>
- </body>
-</html>
diff --git a/elections2/templates/admnewe.html b/elections2/templates/admnewe.html
deleted file mode 100644
index 2fe3b1e..0000000
--- a/elections2/templates/admnewe.html
+++ /dev/null
@@ -1,27 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude">
- <xi:include href="master.html" />
- <head>
- <title>Fedora Elections -- Administration -- New Election</title>
- </head>
- <body>
- <form action="${tg.url('/admin/newe')}" method="post">
- <table>
- <tr><td>Election Name:</td><td><input type="text" name="shortdesc" /></td></tr>
- <tr><td>Alias:</td><td><input type="text" name="alias" /></td></tr>
- <tr><td>Election Info:</td><td><textarea name="info" rows="5" cols="40"></textarea></td></tr>
- <tr><td>URL:</td><td><input type="text" name="url" /></td></tr>
- <tr><td>Start Date:</td><td><input type="text" name="startdate" value="yyyy-mm-dd hh:mm:ss"/></td></tr>
- <tr><td>End Date:</td><td><input type="text" name="enddate" value="yyyy-mm-dd hh:mm:ss"/></td></tr>
- <tr><td>Seats Elected:</td><td><input type="text" name="seats"/></td></tr>
- <tr><td>Embargoed?</td><td><input type="checkbox" name="embargoed" /> </td></tr>
- <tr><td>Allow Nominations?</td><td><input type="checkbox" name="allownominations" /> </td></tr>
- <tr><td>Until When?</td><td><input type="text" name="nominationend" /> </td></tr>
- <tr><td>Use FAS?</td><td><input type="checkbox" name="usefas" /> </td></tr>
- <tr><td></td><td><input type="submit" name="submit" value="Submit" /></td></tr>
- </table>
- </form>
- </body>
-</html>
diff --git a/elections2/templates/error.html b/elections2/templates/error.html
deleted file mode 100644
index 161ad82..0000000
--- a/elections2/templates/error.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude">
-
- <xi:include href="master.html" />
-
-<head>
- <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
- <title>A ${code} Error has Occurred </title>
-</head>
-
-<body>
-<h1>Error ${code}</h1>
-
-<div>${XML(message)}</div>
-</body>
-</html>
\ No newline at end of file
diff --git a/elections2/templates/fedora_login.html b/elections2/templates/fedora_login.html
deleted file mode 100644
index d04da12..0000000
--- a/elections2/templates/fedora_login.html
+++ /dev/null
@@ -1,105 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude"
- py:strip="True">
-
-<py:match path="loginform" once="true">
- <div class="panel">
- <div id="login-box" class="login">
- <h3>${_('Log In')}</h3>
- <p py:content="str(select('*|text()'))" />
- <form action="${tg.url('/login_handler',
- params=dict(came_from=came_from.encode('utf-8'),
- __logins=login_counter.encode('utf-8')))}"
- method="post" accept-charset="UTF-8"
- class="loginfields">
- <div class="field"><label for="login">${_('User Name:')}</label>
- <input type="text" id="login" name="login" />
- </div>
- <div class="field"><label for="password">${_('Password:')}</label>
- <input type="password" id="password" name="password" />
- </div>
- <div class="field">
- <input type="submit" name="login_b" class="button"
- value="${_('Login')}" />
- <hiddenvals />
- </div>
- </form>
- <form py:if="tg.identity"
- action="${tg.url(previous_url)}" method="post">
- <p><a href="http://en.wikipedia.org/wiki/CSRF">${_('CSRF attacks')}</a>
- ${_(''' are a means for a malicious website to make a request of another
- web server as the user who contacted the malicious web site. The
- purpose of this page is to help protect your account and this server
- from attacks from such malicious web sites. By clicking below, you are
- proving that you are a person rather than just the web browser
- forwarding your authentication cookies on behalf of a malicious
- website.''')}</p>
- <input type="submit" name="csrf_login" class="button"
- value="${_('I am a human')}" />
- <hiddenvals />
- </form>
- <ul>
- <li><a href="https://admin.fedoraproject.org/accounts/user/resetpass">${_('Forgot Password?')}</a></li>
- <li><a href="https://admin.fedoraproject.org/accounts/user/new">${_('Sign Up')}</a></li>
- </ul>
- </div>
- </div>
-</py:match>
-
-<py:match path="hiddenvals" once="true">
- <input py:if="forward_url" type="hidden" name="forward_url"
- value="${forward_url}" />
- <div py:for="name,values in original_parameters.items()" py:strip="1">
- <input py:for="value in isinstance(values, list) and values or [values]"
- py:if="name != '_csrf_token'" type="hidden" name="${name}"
- value="${value}"/>
- </div>
-</py:match>
-
-<py:match path="logintoolitem" once="true">
- <li py:if="request.identity" class="toolitem">
- ${_('Welcome')}
- <div py:choose="str(select('@href'))" py:strip="True">
- <span py:when="''" py:choose="" py:strip="True">
- <span py:when="hasattr(request.identity.user, 'display_name')"
- py:replace="request.identity.user.display_name" />
- <span py:when="hasattr(tg.identity.user, 'human_name')"
- py:replace="request.identity.user.human_name" />
- <span py:otherwise=""
- py:replace="request.identity.user_name" />
- </span>
- <a py:otherwise="" href="${select('@href')}" py:choose=""
- py:strip="True">
- <span py:when="hasattr(request.identity , 'display_name')"
- py:replace="request.identity.user.display_name" />
- <span py:when="hasattr(request.identity, 'human_name')"
- py:replace="request.identity.user.human_name" />
- <span py:otherwise=""
- py:replace="request.identity.userid" />
- </a>
- </div>
- </li>
- <li py:if="not request.identity"
- id="login-toolitem" class="toolitem">
- ${_('You are not logged in')}
- <form action="${tg.url('/login')}" method="GET">
- <input type="submit" value="${_('Login')}" class="button" />
- </form>
- </li>
- <li py:if="not request.identity"
- id="login-toolitem" class="toolitem">
- ${_('CSRF protected')}
- <form action="${tg.url(tg.request.path_info)}" method="POST">
- <input type="submit" name="csrf_login" value="${_('Verify Login')}" class="button" />
- </form>
- </li>
- <li py:if="not request.identity"
- id="login-toolitem" class="toolitem">
- <form action="${tg.url('/logout')}" method="GET">
- <input type="submit" value="${_('Logout')}" class="button" />
- </form>
- </li>
-</py:match>
-</html>
diff --git a/elections2/templates/index.html b/elections2/templates/index.html
deleted file mode 100644
index eeadcc1..0000000
--- a/elections2/templates/index.html
+++ /dev/null
@@ -1,113 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude">
- <xi:include href="master.html" />
- <head>
- <title>Fedora Elections</title>
- </head>
- <body>
-
- <py:if test="len(current)">
- <h2>Current Elections</h2>
- <table>
- <tr>
- <th></th>
- <th>Start Date (UTC)</th>
- <th>End Date (UTC)</th>
- <th></th>
- </tr>
- <tr py:for="election in current">
- <td>${election.shortdesc}</td>
- <td>${election.start_date}</td>
- <td>${election.end_date}</td>
- <td>
- <py:choose test="1">
- <py:when test="election.alias == ''">
- <a href="${tg.url('/about/' + str(election.id))}">More Information</a>
- </py:when>
- <py:otherwise>
- <a href="${tg.url('/about/' + str(election.alias))}">More Information</a>
- </py:otherwise>
- </py:choose>
- </td>
- </tr>
- </table>
- </py:if>
-
- <py:if test="len(future)">
- <h2>Elections Coming soon...</h2>
- <table>
- <tr>
- <th></th>
- <th>Start Date (UTC)</th>
- <th>End Date (UTC)</th>
- <th></th>
- </tr>
- <tr py:for="election in future">
- <td>${election.shortdesc}</td>
- <td>${election.start_date}</td>
- <td>${election.end_date}</td>
- <td>
- <py:choose test="1">
- <py:when test="election.alias == ''">
- <a href="${tg.url('/about/' + str(election.id))}">More Information</a>
- </py:when>
- <py:otherwise>
- <a href="${tg.url('/about/' + str(election.alias))}">More Information</a>
- </py:otherwise>
- </py:choose>
- </td>
- </tr>
- </table>
- </py:if>
-
- <py:if test="len(past)">
- <h2>Past Elections</h2>
- <table>
- <tr>
- <th></th>
- <th>Start Date (UTC)</th>
- <th>End Date (UTC)</th>
- <th></th>
- <th></th>
- </tr>
- <tr py:for="election in past">
-
- <td>${election.shortdesc}</td>
- <td>${election.start_date}</td>
- <td>${election.end_date}</td>
- <td>
- <py:choose test="1">
- <py:when test="election.alias == ''">
- <a href="${tg.url('/about/' + str(election.id))}">More Information</a>
- </py:when>
- <py:otherwise>
- <a href="${tg.url('/about/' + str(election.alias))}">More Information</a>
- </py:otherwise>
- </py:choose>
- </td>
- <td>
- <py:choose test="1">
- <py:when test="0 == election.embargoed">
- <py:choose test="1">
- <py:when test="election.alias == ''">
- <a href="${tg.url('/results/' + str(election.id))}">Results</a>
- </py:when>
- <py:otherwise>
- <a href="${tg.url('/results/' + str(election.alias))}">Results</a>
- </py:otherwise>
- </py:choose>
- </py:when>
- <py:otherwise>
- Election Results under Embargo
- </py:otherwise>
- </py:choose>
- </td>
-
- </tr>
- </table>
- </py:if>
-
- </body>
-</html>
diff --git a/elections2/templates/login.html b/elections2/templates/login.html
deleted file mode 100644
index 2fc8b29..0000000
--- a/elections2/templates/login.html
+++ /dev/null
@@ -1,20 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude">
- <xi:include href="master.html" />
- <xi:include href="fedora_login.html" />
- <head>
- <title>${appTitle}</title>
- </head>
- <body>
- <div id="loginform">
-<form action="${tg.url('/login_handler', params=dict(came_from=came_from.encode('utf-8'), __logins=login_counter.encode('utf-8')))}" method="post" accept-charset="UTF-8" class="loginfields">
- <h2><span>Login</span></h2>
- <label for="loginusername">Username:</label><input type="text" id="loginusername" name="login" class="text"></input><br/>
- <label for="loginpassword">Password:</label><input type="password" id="loginpassword" name="password" class="text"></input>
- <input type="submit" id="submit" value="Login" />
-</form>
-</div>
-</body>
-</html>
diff --git a/elections2/templates/master.html b/elections2/templates/master.html
deleted file mode 100644
index f86100c..0000000
--- a/elections2/templates/master.html
+++ /dev/null
@@ -1,81 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:xi="http://www.w3.org/2001/XInclude"
- xmlns:py="http://genshi.edgewall.org/"
- py:strip="">
- <head py:match="head" py:attrs="select('@*')">
- <meta py:replace="select('*|text()')" />
- <link rel="stylesheet" type="text/css" media="screen" href="${tg.url('/static/css/style.css')}" />
- <link rel="stylesheet" type="text/css" media="screen" href="https://fedoraproject.org/static/css/fedora.css" />
- <jsglobals />
- </head>
- <body py:match="body" py:attrs="select('@*')">
- <div id="wrapper">
-
- <div id="head">
- <h1><a href="/en/index">Fedora</a></h1>
- </div>
-
- <div id="sidebar">
- <div id="nav">
- <h2>Navigation</h2>
- <ul>
- <li id="nav-home"><a href="http://fedoraproject.org/en/index">Home</a></li>
- <li id="nav-get"><a href="http://fedoraproject.org/en/get-fedora">Get Fedora</a></li>
- <li id="nav-join"><a href="http://fedoraproject.org/en/join-fedora">Join Fedora</a></li>
- <li id="nav-help"><a href="http://fedoraproject.org/en/get-help">Get Help</a></li>
- </ul>
- </div>
- </div>
-
- <div id="content">
-
- <ul class="toolbar" id="#main-toolbar">
- <span py:if="tg.auth_stack_enabled" py:strip="True">
- <li py:if="not request.identity" id="login" class="loginlogout"><a href="${tg.url('/login')}">Login</a></li>
- <li py:if="request.identity" id="login" class="loginlogout"><a href="${tg.url('/logout_handler')}">Logout</a></li>
- <li py:if="request.identity" id="admin" class="loginlogout"><a href="${tg.url('/admin')}">Admin</a></li>
- </span>
-<!--
- <li py:if="(tg.config.get('admingroup', 'elections') in tg.identity.groups)">Admin</li>
--->
- </ul>
- <div py:if="tg.flash" class="flash">
- ${tg.flash}
- </div>
-
- <div py:replace="select('*|text()')" />
- </div>
- <p>
- Please report voting issues to the <a href="https://fedorahosted.org/fedora-infrastructure/">Fedora Infrastructure ticket system</a> selecting the Elections component.
- </p>
-
- </div>
-
- <div id="bottom">
- <div id="footer">
-
- <p class="copy">
- Copyright © 2008 Red Hat, Inc. and others. Please send any comments or corrections to the <a href="mailto:webmaster@fedoraproject.org">websites team</a>.
- </p>
- <p class="disclaimer">
- The Fedora Project is maintained and driven by the community and sponsored by Red Hat. This is a community maintained site. Red Hat is not responsible for content.
- </p>
- <ul>
- <li class="first"><a href="/en/sponsors">Sponsors</a></li>
-
- <li><a href="http://fedoraproject.org/wiki/Legal">Legal</a></li>
- <li><a href="http://fedoraproject.org/wiki/Legal/TrademarkGuidelines">Trademark Guidelines</a></li>
- </ul>
- </div>
- </div>
- </body>
-
-<!--
- <xi:include href="fedora_login.html" />
--->
-<!--
- <xi:include href="${tg.fedora_template('jsglobals.html')}" />
--->
-
-</html>
diff --git a/elections2/templates/results.html b/elections2/templates/results.html
deleted file mode 100644
index 7570a55..0000000
--- a/elections2/templates/results.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude">
- <xi:include href="master.html" />
- <head>
- <title>${appTitle}</title>
- </head>
- <body>
- <h1>${election.shortdesc} Results</h1>
- <p>${election.description}</p>
- <p py:if="election.url"><a href="${election.url}">[More Information]</a></p>
- <table border="1" cellpadding="1">
- <tr py:for="candcount in votecount">
- <td><py:choose test="election.usefas">
- <py:when test="0">
- ${candcount.Candidates.name}
- </py:when>
- <py:when test="1">
- ${usernamemap[candcount.Candidates.id]}
- </py:when>
- </py:choose>
- <small py:if="candcount.Candidates.url">
- <a href="${candcount.Candidates.url}">[info]</a>
- </small>
- </td>
- <td>${candcount.novotes}</td>
- </tr>
- </table>
- </body>
-</html>
diff --git a/elections2/templates/verify.html b/elections2/templates/verify.html
deleted file mode 100644
index c5b0d59..0000000
--- a/elections2/templates/verify.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude">
- <xi:include href="master.html" />
- <head>
- <title>Fedora Elections -- Vote Verification -- ${election.shortdesc}</title>
- </head>
- <body>
- <h1>Vote Verification Page</h1>
- <p>Please check the below content to ensure that all of your cast votes are marked as 'valid', if you notice any elections showing that you have an invalid vote cast please contact the election administration team by creating a ticket at the <a href="https://fedorahosted.org/fedora-infrastructure/">Fedora Infrastructure ticket system</a> and select the 'Elections' component with your FAS username and the election that is marked as invalid.</p>
- <h2 py:if="validvotes">Valid Votes</h2>
- <ul py:if="validvotes">
- <li py:for="vote in validvotes">
- <a href="${tg.url('/about/' + validvotes[vote].election.id)}">
- ${validvotes[vote].election.shortdesc}
- </a>
- </li>
- </ul>
- <h2 py:if="invalidvotes">Invalid Votes</h2>
- <ul py:if="invalidvotes">
- <li py:for="vote in invalidvotes">
- <a href="${tg.url('/about/' + invalidvotes[vote].election.id)}">
- ${invalidvotes[vote].election.shortdesc}
- </a>
- </li>
- </ul>
-
- </body>
-</html>
diff --git a/elections2/templates/vote.html b/elections2/templates/vote.html
deleted file mode 100644
index 7887c69..0000000
--- a/elections2/templates/vote.html
+++ /dev/null
@@ -1,61 +0,0 @@
-<?python
- def gradient(nocands, voteweight):
- weightpercent = ((voteweight*100)/nocands)
- if weightpercent >=90:
- return "vthot"
- elif weightpercent >= 70:
- return "vtwarm"
- elif weightpercent >= 50:
- return "vtmedwarm"
- elif weightpercent >= 30:
- return "vtmedcool"
- elif weightpercent >= 10:
- return "vtcool"
- else:
- return "vtcold"
-?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude">
- <xi:include href="master.html" />
- <head>
- <title>${appTitle} -- Cast Your Vote -- ${election.shortdesc}</title>
- </head>
- <body>
- <h1>${election.shortdesc}</h1>
- <p>${election.description}</p>
- <p py:if="election.url"><a href="${election.url}">[More Information]</a></p>
- <div>
- <form action="${tg.url('/vote/' + str(election.id))}" method="post">
- <table border="1" cellpadding="1">
- <tr py:for="candidate in candidates">
- <td><py:choose test="election.usefas"><py:when test="0">${candidate.name}</py:when><py:when test="1">${usernamemap[candidate.id]}</py:when></py:choose> <small py:if="candidate.url"><a href="${candidate.url}">[info]</a></small></td>
- <py:if test="nextaction=='vote'">
- <td><select name="${candidate.id}">
- <option py:for="option in range(0,len(candidates)+1)" value="$option">$option</option>
- </select></td>
- </py:if>
- <py:if test="nextaction=='confirm'">
- <td><input type="hidden" name="${candidate.id}" value="${voteinfo[candidate.id]}" /><div class="${gradient(len(candidates),voteinfo[candidate.id])}">${voteinfo[candidate.id]}</div></td>
- </py:if>
- </tr>
- <tr>
- <td><input py:if="nextaction=='confirm'" type="button" value="Go Back" onClick="history.go(-1)" /></td>
- <td>
- <input py:if="nextaction!='confirm'" type="submit" name="${nextaction}" value="Preview" />
- <input py:if="nextaction=='confirm'" type="submit" name="${nextaction}" value="Submit" />
- </td>
- </tr>
- </table>
- </form>
- </div>
- <div>
- <h2>Information on Range Voting</h2>
- <p>Fedora Project has implemented <a href="http://rangevoting.org/">Range Voting</a> for this election, in particular the "Range (score-summing, blanks treated as zero score, no quorum rule)" range voting system.</p>
- <p>To cast your vote in this election simply select a value between 0 and ${len(candidates)} with 0 as 'least or no preference' and ${len(candidates)} as 'highest preference'.</p>
- <p>At the end of the election, the highest ranking candidate(s) are marked as the winners.</p>
- <p>For more information about Range Voting, visit the <a href="http://rangevoting.org/">Center for Range Voting</a>.</p>
- </div>
- </body>
-</html>
diff --git a/elections2/tests/__init__.py b/elections2/tests/__init__.py
deleted file mode 100644
index 6b2c015..0000000
--- a/elections2/tests/__init__.py
+++ /dev/null
@@ -1,64 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Unit and functional test suite for elections2."""
-
-from os import path
-import sys
-
-from tg import config
-from paste.deploy import loadapp
-from paste.script.appinstall import SetupCommand
-from routes import url_for
-from webtest import TestApp
-from nose.tools import eq_
-
-from elections2 import model
-
-__all__ = ['setup_db', 'teardown_db', 'TestController', 'url_for']
-
-def setup_db():
- """Method used to build a database"""
- engine = config['pylons.app_globals'].sa_engine
- model.init_model(engine)
- model.metadata.create_all(engine)
-
-def teardown_db():
- """Method used to destroy a database"""
- engine = config['pylons.app_globals'].sa_engine
- model.metadata.drop_all(engine)
-
-
-class TestController(object):
- """
- Base functional test case for the controllers.
-
- The elections2 application instance (``self.app``) set up in this test
- case (and descendants) has authentication disabled, so that developers can
- test the protected areas independently of the :mod:`repoze.who` plugins
- used initially. This way, authentication can be tested once and separately.
-
- Check elections2.tests.functional.test_authentication for the repoze.who
- integration tests.
-
- This is the officially supported way to test protected areas with
- repoze.who-testutil (http://code.gustavonarea.net/repoze.who-testutil/).
-
- """
-
- application_under_test = 'main_without_authn'
-
- def setUp(self):
- """Method called by nose before running each test"""
- # Loading the application:
- conf_dir = config.here
- wsgiapp = loadapp('config:test.ini#%s' % self.application_under_test,
- relative_to=conf_dir)
- self.app = TestApp(wsgiapp)
- # Setting it up:
- test_file = path.join(conf_dir, 'test.ini')
- cmd = SetupCommand('setup-app')
- cmd.run([test_file])
-
- def tearDown(self):
- """Method called by nose after running each test"""
- # Cleaning up the database:
- teardown_db()
diff --git a/elections2/tests/functional/__init__.py b/elections2/tests/functional/__init__.py
deleted file mode 100644
index 74b869b..0000000
--- a/elections2/tests/functional/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Functional test suite for the controllers of the application."""
\ No newline at end of file
diff --git a/elections2/tests/functional/test_authentication.py b/elections2/tests/functional/test_authentication.py
deleted file mode 100644
index f854a8b..0000000
--- a/elections2/tests/functional/test_authentication.py
+++ /dev/null
@@ -1,85 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Integration tests for the :mod:`repoze.who`-powered authentication sub-system.
-
-As elections2 grows and the authentication method changes, only these tests
-should be updated.
-
-"""
-
-from elections2.tests import TestController
-
-
-class TestAuthentication(TestController):
- """Tests for the default authentication setup.
-
- By default in TurboGears 2, :mod:`repoze.who` is configured with the same
- plugins specified by repoze.what-quickstart (which are listed in
- http://code.gustavonarea.net/repoze.what-quickstart/#repoze.what.plugins....).
-
- As the settings for those plugins change, or the plugins are replaced,
- these tests should be updated.
-
- """
-
- application_under_test = 'main'
-
- def test_forced_login(self):
- """Anonymous users are forced to login
-
- Test that anonymous users are automatically redirected to the login
- form when authorization is denied. Next, upon successful login they
- should be redirected to the initially requested page.
-
- """
- # Requesting a protected area
- resp = self.app.get('/secc/', status=302)
- assert resp.location.startswith('http://localhost/login')
- # Getting the login form:
- resp = resp.follow(status=200)
- form = resp.form
- # Submitting the login form:
- form['login'] = u'manager'
- form['password'] = 'managepass'
- post_login = form.submit(status=302)
- # Being redirected to the initially requested page:
- assert post_login.location.startswith('http://localhost/post_login')
- initial_page = post_login.follow(status=302)
- assert 'authtkt' in initial_page.request.cookies, \
- "Session cookie wasn't defined: %s" % initial_page.request.cookies
- assert initial_page.location.startswith('http://localhost/secc/'), \
- initial_page.location
-
- def test_voluntary_login(self):
- """Voluntary logins must work correctly"""
- # Going to the login form voluntarily:
- resp = self.app.get('/login', status=200)
- form = resp.form
- # Submitting the login form:
- form['login'] = u'manager'
- form['password'] = 'managepass'
- post_login = form.submit(status=302)
- # Being redirected to the home page:
- assert post_login.location.startswith('http://localhost/post_login')
- home_page = post_login.follow(status=302)
- assert 'authtkt' in home_page.request.cookies, \
- 'Session cookie was not defined: %s' % home_page.request.cookies
- assert home_page.location == 'http://localhost/'
-
- def test_logout(self):
- """Logouts must work correctly"""
- # Logging in voluntarily the quick way:
- resp = self.app.get('/login_handler?login=manager&password=managepass',
- status=302)
- resp = resp.follow(status=302)
- assert 'authtkt' in resp.request.cookies, \
- 'Session cookie was not defined: %s' % resp.request.cookies
- # Logging out:
- resp = self.app.get('/logout_handler', status=302)
- assert resp.location.startswith('http://localhost/post_logout')
- # Finally, redirected to the home page:
- home_page = resp.follow(status=302)
- authtkt = home_page.request.cookies.get('authtkt')
- assert not authtkt or authtkt == 'INVALID', \
- 'Session cookie was not deleted: %s' % home_page.request.cookies
- assert home_page.location == 'http://localhost/', home_page.location
diff --git a/elections2/tests/functional/test_root.py b/elections2/tests/functional/test_root.py
deleted file mode 100644
index d912819..0000000
--- a/elections2/tests/functional/test_root.py
+++ /dev/null
@@ -1,80 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Functional test suite for the root controller.
-
-This is an example of how functional tests can be written for controllers.
-
-As opposed to a unit-test, which test a small unit of functionality,
-functional tests exercise the whole application and its WSGI stack.
-
-Please read http://pythonpaste.org/webtest/ for more information.
-
-"""
-from nose.tools import assert_true
-
-from elections2.tests import TestController
-
-
-class TestRootController(TestController):
- """Tests for the method in the root controller."""
-
- def test_index(self):
- """The front page is working properly"""
- response = self.app.get('/')
- msg = 'TurboGears 2 is rapid web application development toolkit '\
- 'designed to make your life easier.'
- # You can look for specific strings:
- assert_true(msg in response)
-
- # You can also access a BeautifulSoup'ed response in your tests
- # (First run $ easy_install BeautifulSoup
- # and then uncomment the next two lines)
-
- #links = response.html.findAll('a')
- #print links
- #assert_true(links, "Mummy, there are no links here!")
-
- def test_environ(self):
- """Displaying the wsgi environ works"""
- response = self.app.get('/environ.html')
- assert_true('The keys in the environment are: ' in response)
-
- def test_data(self):
- """The data display demo works with HTML"""
- response = self.app.get('/data.html?a=1&b=2')
- expected = """\
-<table>
- <tr>
- <td>a</td>
- <td>1</td>
- </tr>
- <tr>
- <td>b</td>
- <td>2</td>
- </tr>
- </table>
-"""
- assert expected in response, response
-
- def test_data_json(self):
- """The data display demo works with JSON"""
- resp = self.app.get('/data.json?a=1&b=2')
- assert '"a": "1", "b": "2"' in resp, resp
-
- def test_secc_with_manager(self):
- """The manager can access the secure controller"""
- # Note how authentication is forged:
- environ = {'REMOTE_USER': 'manager'}
- resp = self.app.get('/secc', extra_environ=environ, status=200)
- assert 'Secure Controller here' in resp.body, resp.body
-
- def test_secc_with_editor(self):
- """The editor cannot access the secure controller"""
- environ = {'REMOTE_USER': 'editor'}
- self.app.get('/secc', extra_environ=environ, status=403)
- # It's enough to know that authorization was denied with a 403 status
-
- def test_secc_with_anonymous(self):
- """Anonymous users must not access the secure controller"""
- self.app.get('/secc', status=401)
- # It's enough to know that authorization was denied with a 401 status
diff --git a/elections2/tests/models/__init__.py b/elections2/tests/models/__init__.py
deleted file mode 100644
index 845220d..0000000
--- a/elections2/tests/models/__init__.py
+++ /dev/null
@@ -1,62 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Unit test suite for the models of the application."""
-from nose.tools import assert_equals
-
-from elections2.model import DBSession
-from elections2.tests import setup_db, teardown_db
-
-__all__ = ['ModelTest']
-
-# Create an empty database before we start our tests for this module
-def setup():
- """Function called by nose on module load"""
- setup_db()
-
-# Tear down that database
-def teardown():
- """Function called by nose after all tests in this module ran"""
- teardown_db()
-
-
-class ModelTest(object):
- """Base unit test case for the models."""
-
- klass = None
- attrs = {}
-
- def setUp(self):
- """Prepare model test fixture."""
- try:
- new_attrs = {}
- new_attrs.update(self.attrs)
- new_attrs.update(self.do_get_dependencies())
- self.obj = self.klass(**new_attrs)
- DBSession.add(self.obj)
- DBSession.flush()
- return self.obj
- except:
- DBSession.rollback()
- raise
-
- def tearDown(self):
- """Finish model test fixture."""
- DBSession.rollback()
-
- def do_get_dependencies(self):
- """Get model test dependencies.
-
- Use this method to pull in other objects that need to be created
- for this object to be build properly.
-
- """
- return {}
-
- def test_create_obj(self):
- """Model objects can be created"""
- pass
-
- def test_query_obj(self):
- """Model objects can be queried"""
- obj = DBSession.query(self.klass).one()
- for key, value in self.attrs.iteritems():
- assert_equals(getattr(obj, key), value)
diff --git a/elections2/tests/models/test_auth.py b/elections2/tests/models/test_auth.py
deleted file mode 100644
index d9d0294..0000000
--- a/elections2/tests/models/test_auth.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Test suite for the TG app's models"""
-from nose.tools import eq_
-
-from elections2 import model
-from elections2.tests.models import ModelTest
-
-class TestGroup(ModelTest):
- """Unit test case for the ``Group`` model."""
- klass = model.Group
- attrs = dict(
- group_name = u"test_group",
- display_name = u"Test Group"
- )
-
-
-class TestUser(ModelTest):
- """Unit test case for the ``User`` model."""
-
- klass = model.User
- attrs = dict(
- user_name = u"ignucius",
- email_address = u"ignucius(a)example.org"
- )
-
- def test_obj_creation_username(self):
- """The obj constructor must set the user name right"""
- eq_(self.obj.user_name, u"ignucius")
-
- def test_obj_creation_email(self):
- """The obj constructor must set the email right"""
- eq_(self.obj.email_address, u"ignucius(a)example.org")
-
- def test_no_permissions_by_default(self):
- """User objects should have no permission by default."""
- eq_(len(self.obj.permissions), 0)
-
- def test_getting_by_email(self):
- """Users should be fetcheable by their email addresses"""
- him = model.User.by_email_address(u"ignucius(a)example.org")
- eq_(him, self.obj)
-
-
-class TestPermission(ModelTest):
- """Unit test case for the ``Permission`` model."""
-
- klass = model.Permission
- attrs = dict(
- permission_name = u"test_permission",
- description = u"This is a test Description"
- )
diff --git a/elections2/websetup/__init__.py b/elections2/websetup/__init__.py
deleted file mode 100644
index 3038dee..0000000
--- a/elections2/websetup/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Setup the elections2 application"""
-
-import logging
-
-from elections2.config.environment import load_environment
-
-__all__ = ['setup_app']
-
-log = logging.getLogger(__name__)
-
-from schema import setup_schema
-import bootstrap
-
-def setup_app(command, conf, vars):
- """Place any commands to setup elections2 here"""
- load_environment(conf.global_conf, conf.local_conf)
- setup_schema(command, conf, vars)
- bootstrap.bootstrap(command, conf, vars)
diff --git a/elections2/websetup/bootstrap.py b/elections2/websetup/bootstrap.py
deleted file mode 100644
index 24050a3..0000000
--- a/elections2/websetup/bootstrap.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Setup the elections2 application"""
-
-import logging
-from tg import config
-from elections2 import model
-
-import transaction
-
-
-def bootstrap(command, conf, vars):
- """Place any commands to setup elections2 here"""
-
- # <websetup.bootstrap.before.auth
- from sqlalchemy.exc import IntegrityError
- try:
- u = model.User()
- u.user_name = u'manager'
- u.display_name = u'Example manager'
- u.email_address = u'manager(a)somedomain.com'
- u.password = u'managepass'
-
- model.DBSession.add(u)
-
- g = model.Group()
- g.group_name = u'managers'
- g.display_name = u'Managers Group'
-
- g.users.append(u)
-
- model.DBSession.add(g)
-
- p = model.Permission()
- p.permission_name = u'manage'
- p.description = u'This permission give an administrative right to the bearer'
- p.groups.append(g)
-
- model.DBSession.add(p)
-
- u1 = model.User()
- u1.user_name = u'editor'
- u1.display_name = u'Example editor'
- u1.email_address = u'editor(a)somedomain.com'
- u1.password = u'editpass'
-
- model.DBSession.add(u1)
- model.DBSession.flush()
- transaction.commit()
- except IntegrityError:
- print 'Warning, there was a problem adding your auth data, it may have already been added:'
- import traceback
- print traceback.format_exc()
- transaction.abort()
- print 'Continuing with bootstrapping...'
-
-
- # <websetup.bootstrap.after.auth>
diff --git a/elections2/websetup/schema.py b/elections2/websetup/schema.py
deleted file mode 100644
index 0bfd27e..0000000
--- a/elections2/websetup/schema.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Setup the elections2 application"""
-
-import logging
-import transaction
-from tg import config
-
-def setup_schema(command, conf, vars):
- """Place any commands to setup elections2 here"""
- # Load the models
-
- # <websetup.websetup.schema.before.model.import>
- from elections2 import model
- # <websetup.websetup.schema.after.model.import>
-
-
- # <websetup.websetup.schema.before.metadata.create_all>
- print "Creating tables"
- model.metadata.create_all(bind=config['pylons.app_globals'].sa_engine)
- # <websetup.websetup.schema.after.metadata.create_all>
- transaction.commit()
diff --git a/fedora-elections.spec b/fedora-elections.spec
deleted file mode 100644
index 5b6e538..0000000
--- a/fedora-elections.spec
+++ /dev/null
@@ -1,80 +0,0 @@
-%{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
-%{!?pyver: %define pyver %(%{__python} -c "import sys ; print sys.version[:3]")}
-
-Name: fedora-elections
-Version: 0.4.0
-Release: 1%{?dist}
-Summary: Elections Application for Fedora Project
-
-Group: Development/Languages
-License: GPLv2
-URL: http://www.fedorahosted.org/elections
-Source0: elections-%{version}.tar.gz
-BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
-
-BuildArch: noarch
-
-BuildRequires: python-devel python-setuptools-devel TurboGears
-Requires: TurboGears >= 1.0.4
-Requires: python-sqlalchemy >= 0.4
-Requires: python-fedora >= 0.2.99.10
-Requires: python-genshi
-
-%description
-Fedora Elections is a web application designed to process and run elections
-for the Fedora Project built with TurboGears.
-
-%prep
-%setup -q -n elections-%{version}
-
-
-%build
-%{__python} setup.py build --install-data='%{_datadir}' --install-conf='%{_sysconfdir}'
-
-%install
-rm -rf $RPM_BUILD_ROOT
-%{__python} setup.py install -O1 --skip-build --install-data='%{_datadir}' --install-conf='%{_sysconfdir}' --root %{buildroot}
-%{__mkdir_p} %{buildroot}%{_sbindir}
-%{__mv} %{buildroot}%{_bindir}/start-elections %{buildroot}%{_sbindir}
-%{__cp} examples/elections.wsgi %{buildroot}%{_datadir}/elections/
-
-%clean
-rm -rf $RPM_BUILD_ROOT
-
-
-%files
-%defattr(-,root,root,-)
-%doc COPYING elections.sql
-%{_datadir}/elections/
-%{_sbindir}/*
-%attr(640,root,root) %config(noreplace) %{_sysconfdir}/*
-
-
-%changelog
-* Fri Sep 17 2010 Mark Chappell <tremble(a)fedoraproject.org> - 0.4.0-1
-- Bump, various GUI improvements
-- New Elections Editing Admin interface
-
-* Mon Jul 14 2008 Nigel Jones <dev(a)nigelj.com> - 0.1.5-1
-- Bump, some minor improvements etc etc, mainly geared towards FESCo election
-
-* Sat Jun 13 2008 Nigel Jones <dev(a)nigelj.com> - 0.1.0-5
-- Strings bump
-
-* Fri Jun 13 2008 Nigel Jones <dev(a)nigelj.com> - 0.1.0-4
-- Template bump
-
-* Thu Jun 12 2008 Nigel Jones <dev(a)nigelj.com> - 0.1.0-3
-- Another bump
-
-* Thu Jun 12 2008 Nigel Jones <dev(a)nigelj.com> - 0.1.0-2
-- Bump
-
-* Wed Jun 11 2008 Nigel Jones <dev(a)nigelj.com> - 0.1.0-1
-- Ready for initial deployment
-
-* Fri Jun 6 2008 Nigel Jones <dev(a)nigelj.com> - 0.0.9-1
-- Bump
-
-* Thu May 22 2008 Nigel Jones <dev(a)nigelj.com> - 0.0.5-1
-- Initial RPM - Ready to go!
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index a9acded..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,29 +0,0 @@
-[egg_info]
-tag_build = dev
-tag_svn_revision = true
-
-[nosetests]
-with-pylons=test.ini
-
-# Babel configuration
-[compile_catalog]
-domain = elections2
-directory = elections2/i18n
-statistics = true
-
-[extract_messages]
-add_comments = TRANSLATORS:
-output_file = elections2/i18n/elections2.pot
-width = 80
-keywords = l_
-
-[init_catalog]
-domain = elections2
-input_file = elections2/i18n/elections2.pot
-output_dir = elections2/i18n
-
-[update_catalog]
-domain = elections2
-input_file = elections2/i18n/elections2.pot
-output_dir = elections2/i18n
-previous = true
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 082c680..0000000
--- a/setup.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# -*- coding: utf-8 -*-
-try:
- from setuptools import setup, find_packages
-except ImportError:
- from ez_setup import use_setuptools
- use_setuptools()
- from setuptools import setup, find_packages
-
-setup(
- name='elections2',
- version='0.1',
- description='',
- author='',
- author_email='',
- #url='',
- install_requires=[
- "TurboGears2 >= 2.1a1",
- "Babel >=0.9.4",
- "zope.sqlalchemy >= 0.4 ",
- "repoze.tm2 >= 1.0a4",
-
- "repoze.what-quickstart >= 1.0",
- "tgext.admin>=0.3.3",
- ],
- setup_requires=["PasteScript >= 1.7"],
- paster_plugins=['PasteScript', 'Pylons', 'TurboGears2', 'tg.devtools'],
- packages=find_packages(exclude=['ez_setup']),
- include_package_data=True,
- test_suite='nose.collector',
- tests_require=['WebTest', 'BeautifulSoup'],
- package_data={'elections2': ['i18n/*/LC_MESSAGES/*.mo',
- 'templates/*/*',
- 'public/*/*']},
- message_extractors={'elections2': [
- ('**.py', 'python', None),
- ('templates/**.mako', 'mako', None),
- ('templates/**.html', 'genshi', None),
- ('public/**', 'ignore', None)]},
-
- entry_points="""
- [paste.app_factory]
- main = elections2.config.middleware:make_app
-
- [paste.app_install]
- main = pylons.util:PylonsInstaller
- """,
-)
diff --git a/test.ini b/test.ini
deleted file mode 100644
index 2dee11e..0000000
--- a/test.ini
+++ /dev/null
@@ -1,26 +0,0 @@
-#
-# elections2 - TurboGears 2 testing environment configuration
-#
-# The %(here)s variable will be replaced with the parent directory of this file
-#
-[DEFAULT]
-debug = true
-# Uncomment and replace with the address which should receive any error reports
-# email_to = you(a)yourdomain.com
-smtp_server = localhost
-error_email_from = paste@localhost
-
-[server:main]
-use = egg:Paste#http
-host = 0.0.0.0
-port = 5000
-
-[app:main]
-sqlalchemy.url = sqlite:///:memory:
-use = config:development.ini
-
-[app:main_without_authn]
-use = main
-skip_authentication = True
-
-# Add additional test specific configuration options as necessary.
9 years, 10 months
[Fedora Elections] #34: "usefas" does not do validation on candidates
by fedora-badges
#34: "usefas" does not do validation on candidates
----------------------+-----------------------------------------------------
Reporter: mmcgrath | Owner: nigelj
Type: defect | Status: new
Priority: major | Milestone:
Component: General | Version: 0.2.1
Keywords: |
----------------------+-----------------------------------------------------
when using 'usefas' for an election, entering the candidates names does
not actually verify the names before saving to database. This causes an
error later when trying to display the names if they are not correct.
--
Ticket URL: <https://fedorahosted.org/elections/ticket/34>
Fedora Elections <https://fedorahosted.org/elections/>
Fedora Elections
9 years, 10 months
Changes to 'develop'
by Pierre-YvesChibon
New branch 'develop' available with the following commits:
commit e1da282f919d2328d0b2c660e0da649318b76043
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 17:24:45 2014 +0200
Ensure adding multiple users are once are FAS users when adding them (if election is set to)
Fixes to https://fedorahosted.org/elections/ticket/34
commit 197b8cc2f985eb8a4a23db44866136d59809916f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 17:20:23 2014 +0200
Check that new user are FAS users when adding them (if election is set to)
Relates to https://fedorahosted.org/elections/ticket/34
commit 654458e717cc2297ac5c8fd2fcecd5e96f318638
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:35:14 2014 +0200
Do not include FAS integration in test coverage
commit 5fbf34ac222e97d1d0218862d77bc2345a52e3e6
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:32:25 2014 +0200
Add unit-tests for the is_safe_url method
commit d0f2ad21d71f17dc2e23bde5d2b6b21a21e2dfb8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:18:11 2014 +0200
Add unit-tests for select voting
commit 63d640f0b716e9c9a7983367c68e9b3f48708614
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:17:51 2014 +0200
Add a max_votes to 2 for the range voting and adjust the unit-tests accordingly
commit d525e27174a9fab0af29d11e3d51ccb348370a39
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:17:05 2014 +0200
Remove checking if the candidate submitted is in the list as wtforms handles it for us
commit a564eee111e6d6f8989f4a560a5da7c5568b048e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:05:09 2014 +0200
Set a max_votes to election #6
commit 9d4fbc9b9c6b58cbc1663ace5d372a8d971425bc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:04:45 2014 +0200
Adjust the unit-tests now that Toshio voted on the election #6
commit 5d28c7e0f060b77bd02e320cd720910c2a5b9625
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:04:23 2014 +0200
Add votes to the last election added in the tests suite
commit 28196c20a8c9ca3a8b39b863a8d123b03497b1c4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 16:04:03 2014 +0200
Add candidates to the last election added
commit ee9f0edcc1d7f693e00f53bf126ea9ec413f2371
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 15:26:53 2014 +0200
Adjust the unit-tests on the UI for the new election added
commit ba3d12aeee12cd81ff1bc98eea3ac484a451d41d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 15:17:24 2014 +0200
Add a new election of select voting type and open
commit 6e4e8dfc2795a8933b996517f3ae756be7776647
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 15:09:49 2014 +0200
Move the simple voting unit-tests to their own file
commit 2553fab3c281a64c08d0c1fd216a02526ac37ce5
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 15:07:54 2014 +0200
Move the voting range election unit-tests to their own file
commit 4eff4b12b5e610ce91d588572096ef9113a38153
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 13:05:33 2014 +0200
Adjust the unit-test now that we are using url_for() to build the links
commit 49f1c20f6395aa242a0286f5c77c85bc82aa3293
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 13:05:13 2014 +0200
Fix the `More information` link to actually point to the about page
commit ad3a236dae5a1cc27c6fe8447bb61a5794029506
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 12:49:06 2014 +0200
Use url_for() in the archive template to build the links
commit 020f728a5edab5adba98ba8f54c09aa5b5920d4d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 12:48:45 2014 +0200
Add link to the about page on the index and open pages
commit ea3921cbdc2d799d60e132426cfc1fc413512c09
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 19:21:09 2014 +0200
Release 2.2
commit eec5f9e659b07605ac986773fe9f0aa16fabce03
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 19:10:55 2014 +0200
Expand the unit-tests to include a test w/ no max_votes set upon creation
commit edf547bd311751b988cb7338f430f317820ab3f1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 19:08:53 2014 +0200
Fix adding voter groups w/o spaces in their name
commit 3024e69564f7c42658880e10a485be2219f4371a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 19:03:53 2014 +0200
Expand the coverage to test for max_votes not being an int and adding lgl_voters groups when creating the election
commit 6df8b940d6fa3738b1077ab8cb5d7ef8e0c8e098
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:56:57 2014 +0200
Adjust the unit-tests now that the range voting relies on candidate identifier rather than their name
commit f3e8b88ba555a81c732339801869ee1265cbb044
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:56:26 2014 +0200
Adjust unit-tests for the archive page
commit 0b55226806d2507150308c6a98b9a6965b890178
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:55:42 2014 +0200
Fix the range voting to be sure the user is not voting on a candidate of another election
commit 68b674fd6016ca1891fc5a99fcddde4fdcfd9cc8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:38:19 2014 +0200
Only run the unit-tests against faitout when the tests are ran in jenkins
commit da8db6dd1f3b9bace02156d56b66b91f04738c6f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:37:51 2014 +0200
Fix the situation where the candidate.name has accent by using the candidate identifier
commit 4c88c6076d0a9008501466d397f821f228b0f1cc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:26:26 2014 +0200
For single vote election, do the FAS integration when building the form
commit a57b481052ea8717f8ed19616af2235f809dea81
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:22:30 2014 +0200
Drop the usernamemap from the single vote election type
commit fcb7a722e2ab88dbdf4b3e2f08aecfa1b77b0b78
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:17:53 2014 +0200
Forward the usernamemap to the render_field_in_row macro to display the FAS user name correctly
commit 2cf9c89da99cbf59f8af0666505af5ea382ebbec
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:17:31 2014 +0200
Use the shortdesc rather than the description in the archive list/page
commit 8a3cab2a37615ee30a1eb40b67dc5485f6cd0586
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:17:14 2014 +0200
Fix displaying the FAS name when the FAS integration is activated
commit 70f6a688ee6c28518b700c4a42af43dd2fd22f58
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Jul 10 18:16:19 2014 +0200
Fix integrating candidate name with FAS when the election is set as such
commit ab67d9da3c91b18b116045738fcb3849775b548b
Author: Chaoyi Zha <summermontreal(a)gmail.com>
Date: Sun Jun 22 07:32:44 2014 -0400
fix unit tests
commit 76a27320602905668da31f0701bd8ee1b13f22da
Author: Chaoyi Zha <summermontreal(a)gmail.com>
Date: Sun Jun 22 07:17:31 2014 -0400
small typo
commit 3cedf3997143424e507e66ed66a66913ecc0bb91
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sun Jun 22 10:28:36 2014 +0200
Prepare running the unit-tests at build time but cannot be activated for the moment
commit 054b9c2fa281b8ad31d966ce5429038df440349f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sun Jun 22 10:26:47 2014 +0200
Include the runtest.sh and nosetests scripts in the releases
commit 7b4005fc1232d93430f2be9fe9d2ec2edc1063b9
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sun Jun 22 09:48:45 2014 +0200
Improve again the description of select voting
commit e0ada592094a8b28d14ebe997b3f154a631c1632
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 18:43:26 2014 +0200
Improve the description of the different election types
commit 81654b38dc56165d3590a55d9de6467600b2502e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 14:12:15 2014 +0200
Bump to 2.1.1
commit 9c383ab6f448c74c5d0ffea498f08dd295326085
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 14:10:51 2014 +0200
Adjust the 1_to_2.sql file for 2.1+
commit 41e11ede6c3400db39424bc9aa36a7c212d194e3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 14:10:37 2014 +0200
Fix missing imports
commit 9a1eb0f39ed97d1927b2c9a7d1c688afd74a1a86
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 13:00:41 2014 +0200
Bump to 2.1
commit 1d0789531dd5f0168c550ae909679227eaae6d63
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 13:04:46 2014 +0200
Install the sample/default alembic.ini file as no-replace
commit 34de8336333deacfc29b44af6ed27773f310e1b8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 13:03:56 2014 +0200
Install the alembic files in the spec
commit 1a7aef3661c3f492cb9b86d858466277693cb7d1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Jun 10 13:03:04 2014 +0200
Include the alembic files in the sources
commit 4ed8622a5b1ff5a3ee52f869d8110ff5d40739b6
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 31 12:55:07 2014 +0200
Take into account max_votes when editing an election
commit 2b3d71b71c9e736d22764706c72e7af86998ecf2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 31 12:54:17 2014 +0200
Take the max_votes into account when creating an election
commit 133e8cd7068ef2cb4a17915dcd88e85adcbec6e9
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 18:20:51 2014 +0200
Small pep8 fixes
commit fa2a81ad31a5d16a0bd0189fff9b7c4e760045f7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:35:50 2014 +0200
Add alembic revision adding max_votes to the elections table
commit e411466e80b773b7621da0a4c6ca5baebe4e3dd2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:36:18 2014 +0200
Ignore alembic.ini files
commit 75d8b8c2d4de5dbaf92d8f652357de9751e9a223
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:35:38 2014 +0200
Add sample alembic.ini file in the files folder
commit d623aa95bfb6cdf4db9f5c044c76a32158372dd3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:35:22 2014 +0200
Add basic alembic structure for alembic integration
commit f3828a81f02cd842720ce0d6efbaaf87aaf66e72
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:30:27 2014 +0200
The simple voting form has no use of the maximum votes/range
commit 3db0f16460486fbd7349556a403316a6c687eea3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:27:27 2014 +0200
Use the max_votes field of the election to set the maximum ranges or vote
This in the range_voting and select_voting type of elections
commit f5151e9d358307818dcfcaed9d5383be9521cfde
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:28:04 2014 +0200
Adjust the edit and view election templates to display the max_votes field/info
commit 49fd4f42cd2c7c35d054c0a6390f3edd2696690b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:24:24 2014 +0200
Add the max_votes field in the election form
commit 66873f7cfd2b57d0bbaecce07ab0356898165341
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:24:13 2014 +0200
Add a max_votes field in the election table
commit 56ba73b73c6f8d933d619a649622f46a09dc46ca
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:14:39 2014 +0200
Add the logic for vote select
For this type of vote, the user can simply select the candidates of
interest eventually with a maximum number of choices.
commit 250dd11bb1176511eba1871052a3f0c14b333673
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:14:17 2014 +0200
Add select voting as an option when creating an election
commit 2ca246e371c48e1c87c8ffab8b5afd71c01c6151
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 16:10:48 2014 +0200
Add a form for SelectVoting
commit 3097ec0cfa752acf583b75968864c7ded5332290
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 15:36:34 2014 +0200
If the election is of type range_3, call the appropriately vote_range
commit 8adf9bac8c829708693b67a614fec181f8abd622
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 15:35:54 2014 +0200
Offer the simplified range voting (max range = 3) in the options when creating an election
commit 57dd8c62e0a8b3bd2ab4dc56d7140663f12cc41a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 15:35:25 2014 +0200
Update the information message according to the type of voting used for this election
commit a7972da3e0638f4e39219b6ec6c55e0abb09a386
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon May 26 15:34:00 2014 +0200
Have the vote_range function accept a max_range argument
This argument is the maximal value the user can give to its vote.
If none is provided, it defaults to the number of candidates which is the
default range voting mechanism
commit 084d142f281f77bfa1a73967a55a3e7ff970e8c8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:11:49 2014 +0200
Simple voting directly use the candidate identifier now, adjust the tests accordingly
commit 57d1d9a1aca15331e0b16af63556c5e69504c0ff
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:09:54 2014 +0200
The usernamemap converts the field label (username) for FAS full names
commit 5e86d4021141061bf2b6ad5a62ef9f0dfff992c8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:09:39 2014 +0200
Make use the usernamemap for range voting
commit 4769fb448f516ddce61f4f034750c4efc92618f0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:08:34 2014 +0200
Allow to rely on the usernamemap also in render_field_in_row
commit f7d5bbb5aad4f141703d2cd7b1b3860cbcb302b8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:06:55 2014 +0200
The form now provides the candidate id directly as value (instead of its name before)
commit fbf477513c8a30aad0ed60c85929adf5b2007263
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:06:34 2014 +0200
Fill in the usernamemap for all the candidates
commit bbabce9d4e0600695d4407c9526160865920645b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:06:06 2014 +0200
Drop the cand_name dict as we already have the usernamemap dict
commit e301f215db0680942be45d58bb8200438bda5f63
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:05:19 2014 +0200
Fixed presenting the fields of the form with their appropriate label/value
commit ee997da69006d26a3aa16588d45f528e64543cba
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:04:38 2014 +0200
If there is a usernamemap specified, use it instead of the field label
commit 3ab325d8def59207966c36f29158b900e09ebd3a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 15:03:57 2014 +0200
No layout change when confirming for the moment
commit 5de933743873487ca7734a6ca308dcc6a17bdc8e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:44:04 2014 +0200
Fix the tests for the change in the simple voting processing and integration of wtforms
commit 6d8882723355289f3d51a532501db4a193468d45
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:43:33 2014 +0200
Ignore the [more] link for the candidates for the moment, we need a better solution
commit d8b8aeb2d76b254db2d00fad1d56749a6b87861e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:43:01 2014 +0200
Use candidate instead of candidates to be consistent with the old behavior
commit a9059c70df20abd3dd2b7251df471cbc7d3ac2b7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:30:56 2014 +0200
Adjust the simple voting template to rely on the form just added
commit 25cca0216f66a99a019e521090d7be59af730bb1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:30:38 2014 +0200
Port the simple voting logic to use the forms just created
commit f6666cd0c3f939c6f85c8a168a2e362cf65c335e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:30:15 2014 +0200
Add forms.get_simple_voting_form that dynamically generate the form for simple voting
commit a1f287519ee095900fe0a4af0b56d81b25f17548
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:19:07 2014 +0200
Rename get_voting_range_form to get_range_voting_form and VotingRange to RangeVoting
commit 5061c4a98c6169400a8d9e7f033730573839c0d9
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:09:16 2014 +0200
Fix the tests for the change in range voting
commit 6811a21f8381b54edd25ddf1d451239e60c59ffe
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 14:08:51 2014 +0200
Allow to vote up to the number of candidates
commit 0f5cf18c7bfc969a8a13c1c12e431e31a1c65fa7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 13:29:14 2014 +0200
Adjust the range voting form to make use of the wtforms form created for it
commit 9d1707b7212a7bf90a67e5509f7373803b04c59e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 13:28:06 2014 +0200
Update the range voting method to relying on a wtforms form
This adds CSRF protection to the voting pages and takes care of the data
validation directly in the form.
commit 8ff016e89f22613e229853611193ef9ff9594225
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 13:26:57 2014 +0200
Add macro to render the choices when confirming a vote
commit e89ddba7b4e08e09ead96db47d2833344afa71a8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 24 13:26:32 2014 +0200
Add dynamically generated form used for range voting
commit b9d18e6bb109a328e15c3ab29f49e8138719a5fa
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu May 29 18:30:17 2014 +0200
Let's be a little more precise/concrete about what the check allows
commit 0b10c912df80c4aef2bba1555f455a15255dd542
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu May 29 18:25:20 2014 +0200
Comment the logic of the conditions to select where to place the limit
commit 27d811cd65391f3a866d732a111a2ea6f1c67049
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 19:00:25 2014 +0200
Show the actual results in the result page
Rework the result table to display a nice red-bar where the limit between
the elected candidates and the non-elected candidates is.
commit 391a1633463fdec9f7a8e4c5c46ac1341cc598a8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 18:58:32 2014 +0200
Avoid adding empty legal voter groups
commit 5948df55e04a6a58ccc1d969aac2b5b6de784997
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:57:49 2014 +0200
Cover adding admin groups when testing admin_new_election
commit 482b27808dd3c56c77ab54bcb0c5c064976e060e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:55:37 2014 +0200
Split the tests for editing admin groups or legal voters into their own method
commit a4cf8fee3d2eb839fdc05ef00935d40fd51cf390
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:50:56 2014 +0200
Add test checking the behavior when the user is not part of the LegalVoters
commit 9db4e630fadff86da79e8ca5fb6eda0cca642ecb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:48:28 2014 +0200
Add test for editing an election with legal voters
These tests are checking how the application behaves when we add or remove
legal voter groups
commit 9e97080a373fa87b4e8823673ddd54e81efcbaa4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:48:04 2014 +0200
Adjust the tests so that the FakeFasUser is part of the group allowed to vote
commit d0ca20d91c0d8cdf4e3d41c97c2e1734e8801276
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:47:48 2014 +0200
Specify a group of user allowed to vote on election #3
commit 91e09ae7a6d56d9d81e1bbccbcd2f6b566c921f0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:37:20 2014 +0200
Add test checking the error message if the user has 0 groups
commit ee565606178a3d8723128b579a580ea388c5add7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:36:39 2014 +0200
Fix the unit-tests so that the FakeFasUser used has at least 1 group
And fix checking the error message with cla_done = False
commit c7d032be2e9309d41f0c13f44c65c9109988a120
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:34:49 2014 +0200
Fix the @login_required to check to cla_done and groups > 0
This commit fixes @login_required that was checking cla_done and returning
an error message about CLA+1.
We now check cla_done and groups > 0 and report an appropriate error message
in both cases.
commit 21120e7c6c1276f57cb54e7f75224990e4a3db64
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:30:52 2014 +0200
Add test for the redirect from the voting page is the user did not sign the CLA
commit 54fc52de4aba4ab46e84ab2554bccc1f9059c84c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:12:20 2014 +0200
If there are legal voters and the user is not one of them, redirect him/her back
commit e5af563f7fbebe59667a172e5ec1e6ba998bb5c6
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:10:59 2014 +0200
Use safe_redirect_back instead of forcing the return to index
commit 592dd35a2f2a52b6ef7b5b57f91c2bd5dcdfea5a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:08:03 2014 +0200
Voting to an election requires CLA+1 at minimum
commit 4569e790f12eede9d634b26eb6ee6b15345e0fab
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:57:02 2014 +0200
Implement legal_voters adding/editing/deleting
This allow the admin to restrict the election to one or more FAS groups
commit 1d7264f803e4e654696d46f112baf992be485489
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:25:45 2014 +0200
Simplify vote_range and vote_simple since they are no longer flask endpoints
- We can directly give them the proper election object, no need to query
it twice
- We can skips the tests we do earlier (avoids doing them twice)
commit 886418b0c3878a8bfe470b73ed8b9aa116b6fd7a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:22:04 2014 +0200
Adjust unit-tests to the unique URL for voting
- Replace vote_range and vote_simple by vote
- Do not test for redirect as it will always return the proper view
commit 8df30094f79b02ca64d451a30428dcbb750e320a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:21:40 2014 +0200
Adjust the action url in the vote templates now that there is a unique URL for voting
commit 60e9ce8e58af5a14c082bdf3b5d67e8be3ad4895
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 17:03:07 2014 +0200
No need to have different URL for the different voting type, one is enough
commit 9f9e14539c552fc57474eb7f78d42777eb10b44a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:36:36 2014 +0200
Explain a little what the admin groups are
commit 7e183b233edf8c75d8c497651b1d867c6e943723
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:29:35 2014 +0200
pep8 fixes
commit 2f8d08da49c29f86dd8ee51abb14b9c126807643
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:28:52 2014 +0200
Add unit-tests for the is_safe_url method
commit ea8695b071a6f9b511a1b35b4a88423ffae7fbfa
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:22:06 2014 +0200
Add unit-test testing the addition and removal of admin groups when editing an election
commit 95180f86f8f4f2fe74fe42a3fcb09b37825c923a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:21:56 2014 +0200
Sort the election's admin groups
commit afe99b9ff2c75fa00d0c7b131ba354aba0258304
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:16:28 2014 +0200
Test the is_election_admin when the user is and is not in an AdminGroup of that election
commit 0dad615372f1185b2323492826352cce24680bfc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:16:15 2014 +0200
Specify an admin group for the election #2
commit 20270e439058593b3fa4338f1abf35607b34ece5
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:10:12 2014 +0200
Enhance coverage of the is_admin function
commit c950dd70fb4438ff3ef562abc126ea0e6dafdd14
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:08:22 2014 +0200
Elections admins are now admins of every elections
commit a8a1cf256906570a686a80402b454e3a1c2048f6
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:08:07 2014 +0200
Make the tests pass offline
commit 5f15df340483eda18948bf86a658ce878f583e5c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 16:00:34 2014 +0200
Implement the election admin groups
This allow the election admin creating the election to specify multiple
FAS groups which will be able to view the results before they are made
public without having to add them to the election admin group itself
commit 64ef428881318f6681309cb505836e04c1f5b5ad
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 23:16:09 2014 +0200
Fix the reverse proxy
commit 5f4fb08d72b731641983d90ca2e91ba5a19bbca1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed May 21 11:24:57 2014 +0200
Add elections.proxy and set elections as an application behind a reverse proxy
This will ensure that flask.redirect is using ``https`` where asked.
Fixes https://fedorahosted.org/fedora-infrastructure/ticket/4369 for elections
commit c396bfee925695b69dcc4af40feb60b6d2f64d4f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 3 16:06:22 2014 +0200
Remove un-used script
commit b9239c3471ee5e675781603625b581c5e1f90d68
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 18:31:53 2014 +0200
'##' is not allowed, and @abadger is right '# #' doesn't look right either
commit 463a39149837573e5a54b6ccdc1d495369427209
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 18:30:04 2014 +0200
Change comment as suggested by @abadger
commit 4edf054bed02df39f38c6b6eae7372b50de0ab29
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:51:45 2014 +0200
pep8 fixes on the tests
commit f90a3e1244f61f9e02f9be89c226408c53cf688a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:49:19 2014 +0200
pep8 fixes on setup.py
commit 2a6f132b4eb2152def61338ac32bfa4f0f627aed
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:49:12 2014 +0200
pep8 fixes on fedora_elections
commit ed30baeed1586ce6a6ed1240d0b8d4db4c85eadc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:45:50 2014 +0200
Convert the boolean into integer as in the DB model (and required by postgresql)
This should fix: http://jenkins.cloud.fedoraproject.org/job/elections/3/
commit 4085c81b5fe9638f1d8f7a28d05f47a78aed0e5c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:37:43 2014 +0200
Adjust the requirements file
commit 27a626c95140c403690febff21ae8fbe19eba22a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:24:41 2014 +0200
Specify where to find the tests so that python setup.py test works
commit e1743b69b8fb3c817b4c7f99762e03bbe66ff724
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri May 2 17:20:25 2014 +0200
Fix dependency in setup.py, the package is named kitchen not python-kitchen
commit 335216a362dfa195db658c79861152e910147bff
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 13:26:17 2014 +0200
Fix the database session for the forms, if we want the tests to run :)
commit 3003ed990250b1119632f5b5c167762e6e852cc1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 13:25:43 2014 +0200
Add tests for the form fields validations when creating a new elections
commit 2d559a4511709dd39f9f76e23a59c518e90a7bbc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 13:24:49 2014 +0200
Add missing import and fix the validate_shortdesc to actually be used
commit f9135b01ea708d06b0d6f39d880471bc8eb9e6e0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 13:00:11 2014 +0200
Add a little bit of documentation in the admin tests
commit f3058767377893a8dc721f7217a557fb92e5a934
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:56:56 2014 +0200
Fix import and session for the unit-tests for the elections and admin controllers
commit d7de79a37e44daac1f5735680fdca423daac6f33
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:56:44 2014 +0200
Add unit-tests for the admin controller
commit c01c453d657135dc6a172d9b3ccfa097e3162ae0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:55:46 2014 +0200
Update the behavior of the ForeinKey upon update or delete
And move the declaration of the relationships after the declaration of
the columns
commit f0e2c775698cef9196747bcf843740ea1b21ccee
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:55:16 2014 +0200
Handle error if someone tries to delete a candidate that already has votes
commit a7d305014f1d7c4690814cac017404018641e2b6
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:54:38 2014 +0200
Add couple of candidates to the election #4
commit f14454d042f8a9935bcc3b8c840c83465163af46
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 12:54:27 2014 +0200
Remove un-used imports
commit 0d4a3b8c4fba707172c8c5a811afe1e85d391503
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 11:23:26 2014 +0200
Add unit-tests for the elections controller
commit 4e28e9794b06919b04b6b3f6a01a94e1f0724b55
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 11:23:11 2014 +0200
One more place where we do not want to cover FAS integration in the unit-tests
commit 2428ccc7978e346a61b64c88e63ddcea02782386
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 07:45:08 2014 +0200
Ignore in the unit-test the possibility that get_valid_election does not return an election
commit 348390a034ea613436cedec7e53566e0b1f93211
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 07:44:20 2014 +0200
Ignore FAS integration for the unit-tests
commit db25b04e2d35c257744c6735d1dff52e75438340
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 17 07:43:40 2014 +0200
Fix the simple voting, make sure the candidate submitted belong to the election
And handle case the candidate id submitted is not an integer
commit 8d3c1855558a9fc914c962f7eec1cdd8b1db2a0b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 20:07:42 2014 +0200
Handle the case the id submitted is not an integer
commit a1d3247c1f1cb227b24d1d83b9540f96371b6aa3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 17:47:40 2014 +0200
Ignore FAS integration for the unit-tests
commit 81b738881b226f502230f6887f4c08ff3cf036cc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 17:47:35 2014 +0200
In case the input is invalid (not an int), the next_action is vote
commit c9c2e293dfc0c193cd4719a7dc1da6e2419eeb3d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 17:31:18 2014 +0200
Fix typo in the Preview (in case of wrong input)
commit d5a6b63969665452869e7d42321b289973dc514e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:54:47 2014 +0200
Ignore case where we would not get a valid election object from get_valid_election
commit 4a1e7f0d8f5d7d7e75249c6780463a4b3fa06301
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:54:03 2014 +0200
Adjust the candidate.id for the candidates that were added to elections #3
commit eaacafa2e7474483a78032fbd8e1e219f7cdd461
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:53:44 2014 +0200
Add candidates to the election #3 and adjust the candidate.id
commit a1b4dd86cf631ca5b476ce26bda40fc784da0d41
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:43:57 2014 +0200
Ignore case where we don't know the election's voting type for the unit-tests
commit 0f0999cdc8bfc66340cb01c3d8f626637542f123
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:39:03 2014 +0200
Adjust the flask tests to new election added
commit d72c0b774fd7464f084ae2654bf7f2687ebdf7de
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:38:40 2014 +0200
Add missing shebang to the runserver.py script
commit b405431a6f53baf4688066cfb6251d7bdab45784
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:38:19 2014 +0200
Add votes to the new election #5 with simple voting
commit a9a88d19695e8ab35d5b886d0c0d96aec4f8bef1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:34:38 2014 +0200
Add candidates to the new election having simple voting
commit 2108d3f0b011e134737fd179384f29f9c3434519
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:33:55 2014 +0200
Add a new simple and open election and adjust the tests to this election
commit 2360918db565875762492b2cba255fcb6958075f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:21:46 2014 +0200
Add missing import in the elections controller
commit 1c471653aca8a1387e707369d4ee62cb40ad5887
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:21:08 2014 +0200
Add missing spaces in flashed messages
commit e1cedcb0f8429309549e845045f25c85181f3ae5
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:20:14 2014 +0200
Remove double spaces on a flashed message
commit 643f6b5bc0307aae55eb67a5b20a8961d558128f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:19:04 2014 +0200
Do not show voting page on election embargoed, these are just ended embargoed
commit 601adeee338e5c68e2de6cc202bb4c0c5268ed7c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 16:05:14 2014 +0200
Update the auth_login method to remove duplicated checks
commit 886856273c7e7a62b70449648cae6301ccb79ffd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:56:03 2014 +0200
Port the flask applications to the ModelFlasktests class
commit 322900eceed396e8df3d29c60a62d148020b4963
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:55:43 2014 +0200
Add a generic unit-test class for the Flask tests
commit d3264a500fbeb29872d76c7792c927e99046abdd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:23:40 2014 +0200
Ignore fedmsg in the unit-tests
commit 5d541bc49e142b9f1d61843ae36e866e5796ad08
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:22:05 2014 +0200
Add unit-tests for the generic flask endpoints
commit 77120e65584ef4a0ed22eb6b6d344ef97e1b783c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:21:11 2014 +0200
Add some more votes on the election #3
This way we have someone that voted on an open election
commit 0694480aa537e1e26ccf28454182d4630e81d67a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 15:20:50 2014 +0200
Ignore some other lines in the unit-tests coverage report
commit 5b6cf8406f527ae32e4bc099f7b6192ddc7ebcb7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 14:44:27 2014 +0200
Add missing docstrings
commit 3417ef13a19fbb7213391a772b9d95829af51f98
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 14:42:39 2014 +0200
Ignore reading the config file specified by environment variable
commit 0f7ab3f47f16be433e191e7b552837542ff935bf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:28:29 2014 +0200
Ignore the .coverage file created when running the tests with coverage
commit ff2512d044efea1642f3be0f6459c14ab8e6e81d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:27:54 2014 +0200
Add the runtest.sh script for easy testings of the project
commit 78bd31f3613a2f295aa2941a4239992172c42d99
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:27:32 2014 +0200
Add a local nosetests script ensuring we run the proper version of the libraries
commit 5b8520b7984c07b6ecc0e79af8cfa2aa7d4693e9
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:27:17 2014 +0200
Add unit-tests for the Vote object
commit cbc05d5e9efe477d87fe941d9dcc0d921ad9a822
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:27:03 2014 +0200
Add unit-tests for the Candidate object
commit d065f1dc564f4c42f3dc8c7bdc8e89ceedacb706
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:26:34 2014 +0200
Add unit-tests for the Election object
commit ff4a92f6ae64458d338eae2d2e041a6558c8065f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:25:29 2014 +0200
Add first code for the unit-tests
Create the basic `Modeltests` class that all the tests will inherit from
Create basic functions to be used when testing the flask application
commit c0fcc6dba773cc23c3086dbd8641dbfb996e2d65
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:24:58 2014 +0200
Ignore a small piece of the createdb function in the unit-tests
commit 10b7b4dd5a7dc557a42f415ab382add5a3bbcf67
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:22:11 2014 +0200
Order votes by timestamp
commit 7f3421ac5bd012e04867c4bef414d70288d64efd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:12:17 2014 +0200
Format the start and end dates in the json representation of an election
commit a767d3636837ceb540cff432c05b179c24478903
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:09:33 2014 +0200
Add default datetime in the timestamp field of the Vote table
commit 596188224686044608755f7c6258c4082fbca1f4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:08:34 2014 +0200
pep8 fixes on the controllers
commit aaf5a1bf1446c0961428a1b887c77107c816dbb5
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 11:08:34 2014 +0200
Make the minimal flask version be 0.10
commit b54707b48442a34f1357b94882a9aed31474f31d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 11:05:52 2014 +0200
Remove current tests
commit 5fef3e05a9dcc71949a3caa3c80d1133ed3e788e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 14:11:06 2014 +0200
Adjust the database mapping of candidates_are_fasusers to its type in the DB
commit bab918725b43cb1a25c2d15d440adcd8e8e9800a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 14:05:09 2014 +0200
Rename the column usefas to candidates_are_fasusers
commit eae19ae58c000c08abe70454e71b58a4846444bc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 12:21:10 2014 +0200
Remove import of the redirect file since the file no longer exists
commit 64d18ad2ea0af70ffd1b5f1f603e670143bae73e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 11:04:14 2014 +0200
Make the runserver.py file executable
commit 0d2e4a2ba7fd571a4b572ca34ee657fc4a2b16cb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 10:47:35 2014 +0200
Give a warning information message to admins seeing an embargoed election
And format a little the warning messages (as orange)
commit 22ecaab36ee797d4c8ffe3e723e64ba877400c2e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 10:34:38 2014 +0200
Split the one controller into three
- __init__ has the main endpoints where no login is necessary
- elections has the elections related endpoints (voting, results...)
- admin has the administration related endpoints (create, edit, delete...)
commit 16b81168cc914d2ae92ea3af7ba796bb838a7b0b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 10:34:20 2014 +0200
Remove the redirect.py file
commit 29a5cdf9badaf5c9f0e386f247d74a3c17e6d717
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 09:46:06 2014 +0200
Adjust the flask application for the new template paths
commit 75fa83d7a06fd33dd9e1f76155d396c0e2adb9a2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 09:44:09 2014 +0200
Move the templates in the `election` folder one level up
commit b40374022aab739390f05de900ab92fad34ec597
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 16 09:43:46 2014 +0200
Move the index template one level up
commit c32c678e06b80627f2387a2d6f55a86fc8af94dc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 10 13:49:47 2014 +0200
Publish fedmsg messages upon adding, editing and deleting candidates
commit 33049c3ec5446e3aef79dedbf10035d639d071e8
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 10 13:49:23 2014 +0200
Add method to convert a Candidate object into a json representation of it
commit c2f34d0d6d429ba934befcdcd91fbe9df170e3f3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 10 13:28:13 2014 +0200
Fix the conversion of the election objects into a json structure for fedmsg
commit d64c2469bd67fa3c4827de7824a0194d96e249b0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 19:47:44 2014 +0200
Broadcast on fedmsg when an election is added or edited
commit b819dff43d8537d0eacf837b36ee6e1d83be223c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 19:47:24 2014 +0200
Add a json compatible representation of an election object
commit 03d975e8c5575cc0fa10a2a6336d08e8b4427085
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 19:47:07 2014 +0200
Update the content of the fedmsg.d folder, simplify it
commit 64e3783b7f7a44afb1ef438ec9d8de084b052b80
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 3 12:42:38 2014 +0200
Bump to 2.0.2
commit 5ae899dddb2452baba9990387ae7f354d1fd696d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 3 12:41:16 2014 +0200
Fix the is_safe_url method, the import differs here
Signed-off-by: Pierre-Yves Chibon <pingou(a)pingoured.fr>
commit 7590a412bee331b738d6ec552d0579d521ed844e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 3 12:23:31 2014 +0200
Bump to 2.0.1
commit 6dc16a411c54055dba62a1ddd4737b6fff7c05be
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat May 3 12:20:09 2014 +0200
Fix redirection bug in the login mechanism
commit a10ef87209a994f3d69ef57f0e29c9a965c74672
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 18:25:34 2014 +0200
Update the changelog date for 2.0 and changelog content
commit d5d287b0b3bcf7c92bec837eea1bda1b6e58c269
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 15:25:25 2014 +0200
Install the update_1_to_2.sql upgrade sql script to /usr/share/fedora-elections
commit 949f51fdc919644f9bf7932cb251f85e60b16c21
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 15:22:20 2014 +0200
When retrieving vote counts, only consider votes above 0
commit 252b6ad7b39ba6f4fbd9f13c4a3c208fc289f5e0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 15:10:24 2014 +0200
Little css fix to have the same padding on each side of a table cell
commit aaa0d3a17c2bf2bbf783d47e5a3a9519cce2fc95
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:51:12 2014 +0200
Adjust the `Vote now!` links in the open elections tab
Now the `Vote now!` links are only shown if the user has not already voted
on the elections and if logged in, otherwise the links are shown
commit e4da32f3714c46b22524b79ed249c600c869438c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:48:16 2014 +0200
List all the elections to the admins
commit 541acaea6ecf57f4272cf05c9e4200fd0f6f1a3c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:47:39 2014 +0200
Order the results returned by elections.search() by election date (most recent first)
commit 5fdc2ff6d4aad1db097e5dc367a98a80ed3e46e3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:34:07 2014 +0200
If the user is authenticated, only show election that the user can vote on
If the user is authenticated, retrieve the list of elections the user has
already voted on, this list is then used to only show the link `Vote now!`
on elections the user has not already voted on
commit 5563ac9b53533ba42b18832f92288f576ad3a4a3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:25:20 2014 +0200
Set flask.g.fas_user to None upon logout
commit 1dd33cd7e5142b85ab8363393da9a102a6538dd1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 11:24:47 2014 +0200
Add a dedicated is_authenticated method and use it in the decorators
commit e22989e22bda006680fa66120aa0f8525f74e07f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 09:55:33 2014 +0200
Add sql upgrade script, allowing to convert the database from elections v1 to v2
commit 8828a99bc94937aba2ab6d0e94400c0795a80526
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 09:50:10 2014 +0200
Fix ordering of the imports
commit 172705470099f79a1e79275664779a6f87282d99
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 09:48:57 2014 +0200
Fix import the configuration file specified by the environment variable
commit 0ff423eece62b682a11417e1186c86b8287b115b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 09:33:56 2014 +0200
pep8 fixes
commit f3822681249c696d2e801628f1f84516911d08f2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 09:31:55 2014 +0200
When importing multiple candidates flash the number of candidates imported
It is simpler to check if all the candidates were imported correctly and
easier to read.
commit 3b425fdf6531623e7fcd8f67015283ed08615341
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:53:53 2014 +0200
Cover case where there has been 0 votes, thus fixing the division by 0
commit 01b2923f9e5300db632dd3dfb24b26547800f4ab
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:53:16 2014 +0200
Give the total number of candidates in the view_election table
commit 8f69bc5388e1ba6c291261f6e60c8dab7858d13c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:53:01 2014 +0200
Fix the default title in the base master
commit e914ab8ed631981fdcb229c63537f2c3bb8130fb
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:52:32 2014 +0200
Fix the order_by date to order in the dates in descending order
commit 44a76c1c3570f198fb8438bb16293339bfaee29f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:50:25 2014 +0200
Change the __tablename__ from electionadmingroups to electionadmins
electionadmins is the name of the table in elections 0.x, so let's be
consistent with it instead of requiring a rename of the table on the
database
commit c1f2f2bba9f305688f51c0d414444fc800927dd2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:48:22 2014 +0200
Remove the `role_required` field in the electionadmingroups table
This field does not exist in elections 0.x and we do not have the
information to enforce the check anyway
commit 0bc6fb68cd65719aaabe04ca5645a7ab3c0c6040
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:43:27 2014 +0200
Change the embargoed field from Boolean to Integer
The embargoed field is an Integer field in the elections 0.x database
so revert elections 2.x to the same field format
commit dfea6d05f31a04e9aabe2325699d97648c5db60f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:41:05 2014 +0200
Rename the number_elected back to seats_elected in the elections table
The field number_elected was named seats_elected on elections 0.x so
let's rename it back to it to avoid changes when updating.
commit 70efbf0423542cbb150a43f21931b5d89c156837
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:37:59 2014 +0200
Remove the frontpage field in the elections table
commit 8485b0d3b077be45290ca07e4fafeef3480b62b4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Apr 9 08:25:34 2014 +0200
Rename the field `election.summary` into `election.shortdesc`
shortdesc is the field used in the elections 0.x, let's keep using it to
reduce database change when updating to 2.x
commit d6e56d0857713d896ae8be0645230041b1b4d137
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Apr 8 17:50:08 2014 +0200
The first release will need to be 2.0
commit 1b818c871f5aeb243b2192c9251679e2246e0bfa
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Apr 8 17:49:16 2014 +0200
Include /usr/share/fedora-elections/ in the spec file
commit 3a35333eb6ec75bb480c6712873c3cea6290f99c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Apr 8 17:48:50 2014 +0200
Adjust the spec file to correspond to the configuration file
commit bac22ba4ae8a224ef763f2ea6ecd5f73712e541d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Apr 8 17:47:21 2014 +0200
Fix the createdb.py script regarding jinja2
commit 8827077137926c4c1a73ed56d6231339b7007296
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon Apr 7 22:42:33 2014 +0200
The spec file has moved to the files folder
commit c961125a02b760df72142f540a3f34b94cd5195c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Mon Apr 7 10:25:12 2014 +0200
Add example configuration file explaining as well all the configuration keys
commit 0174e654b3caf5da905f7ac278cbd23aef5eb63d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat Apr 5 10:31:15 2014 +0200
Simplify the .gitignore file
Most of these files are and will not in this directory
commit ed9514adb75cbf814076630450389607053831fc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat Apr 5 10:29:22 2014 +0200
Ignore the build and dist folders from the sources
commit 52a5cba80f42056842e6a8441050d03c8e7459f0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Apr 4 10:28:31 2014 +0200
Update the spec file to the changes made in the setup.py and directory structure
commit f90fb50ca4fde0b6225e5a29da26af6b2382094c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Apr 4 10:18:47 2014 +0200
Include a MANIFEST.in files listing which files should be present in the releases
commit 8cc8e590cfd45f1ba38b63c910771f22f00661fe
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Apr 4 10:18:41 2014 +0200
Update the setup.py script
- Update the list of dependencies
- pep8 fixes
commit 13a840f6d7e39f21de2a1027a643784504b6a92e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Apr 4 10:40:38 2014 +0200
Remove configuration key that is not used anywhere
commit d89d6a63d869f013dec7090860b2e3b0adc83bf5
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:14:29 2014 +0200
Remove the `shell` script now that there is no longer development.cfg file
Since we have no longer `development.cfg` file, the shell script becomes
a simple call to either ipython or python, so there is no longer interest
in keeping it around.
commit 5bb35e30742c8e0426a6744b24571ccedc692042
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:13:09 2014 +0200
Rename the example wsgi file for fedora_elections
commit 0440e7d01bddcd82225d89aacdcc5eee332c48c0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:12:39 2014 +0200
Small adjustments to the example wsgi file
commit ab2c9025574ef3165d5ff391110390af6f70ebbf
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:10:39 2014 +0200
Rename the example apache configuration file
commit ccbdc1c3fab7f02e6660798c4cd6af8190ebb263
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:09:51 2014 +0200
The apache configuration file should be commented out by default
We do not want to have the application running by itself after having
installed the files/rpm.
commit 50fac823bbc76620572625b2e3a344f41c43b39a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:07:05 2014 +0200
Remove the no-longer needed configure script
commit 7d7ecb548e82077863113cd439144e3fee814386
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:06:33 2014 +0200
Ignore .egg-info files in the git
commit be012883e0bfacb0862efbfd78150d0ba20e7d71
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Apr 3 11:05:59 2014 +0200
Move the spec file and the sample configuration file into the files folder
commit 9bdd75533511a4c4055538bda66320c879bbda4c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 17:07:36 2014 +0100
No need for the runserver script, elections can now run with the default configuration
commit 948e0b2172f5b461407ff6937338f889e3d73b1b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 17:04:48 2014 +0100
Add a generic is_election_admin checking if the user is in the admin group set for an election
Use this generic function when checking if the user is allowed to access
embargoed results.
commit f03be6fe791e02dbece28de0582491dad698ccbd
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 17:01:53 2014 +0100
Rename the is_elections_admin function to is_admin
This is more appropriate since that function relies on the admin group
set in the configuration file.
is_elections_admin should rely on the information set on the database
in relation to a specific election.
commit 0e9dcb4ddde797d2b0d6d0a8591a09981841bc3d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:34:23 2014 +0100
Clean configuration file
- Change from fakefas to stg
- Remove username
- Remove password
- Remove unused configuration key
commit 2503679ec209f6a9aaabcd2a2df5465383ae5be1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:30:34 2014 +0100
Fix the FAS2 client insecure settings
The boolean in the configuration file specify whether to check or not
the ssl cert, but the insecure works the other way around.
If you want to check the ssl cert, you do not want to be insecure and
vice-versa.
commit 6fabe4224fbedb92fd3b66145b21be7cd901ef61
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:13:33 2014 +0100
Fix the status property of an election
In the election creation form we indicate that the end_date is included
in the voting period (vote ends on end_date at 23:59:59).
This commit fixes this by checking dates instead of date/time and making
sure the end_date is included in the voting period.
commit b819e6fcc23e5d809a34577b75570f36d7ac4fa3
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:08:57 2014 +0100
Provide in the election result page some statistics about the election
commit c1ae7d4193b04de6c0d19056563cb7bfb5ce44fc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:08:31 2014 +0100
Add method in the Vote class to retrieve some stats on a specified elections
commit c622d533ccf7ee080afefce453fc9f478c555f39
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:06:50 2014 +0100
The FAS client is FAS2 not fas2
commit 14218287820e0d8a1f7bdf656db37ed76d2d9d63
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:05:46 2014 +0100
Small style change and add information message when displaying embargoed results
commit 1ee68bd081697057fbf7aebab2d6f21af31ace60
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:04:18 2014 +0100
Mark with a red starts the field mandatory when creating an election
commit da248343121b0074e64390da455243738c6f2265
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Mar 27 16:03:54 2014 +0100
When running elections2 locally, activate the debug mode
commit 4bb4212a5df890987abe8f6d0ac0765aca134bb7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Tue Apr 1 08:09:14 2014 +0200
Specify clearly that URL are optional when uploading multiple candidates
commit 9cdbf0d7e5cc7c904bdbfebd8eecd6f9b29a1084
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Feb 20 12:04:07 2014 +0100
Add link on the admin page of an election to import multiple candidates
commit 2267ed30df3358ae9316bc1a38ddb5d4b322dc99
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Feb 20 12:03:19 2014 +0100
Add the controller to import multiple candidates in one go
Fixes https://github.com/fedora-infra/elections/issues/5
commit 3dfb4badea57f434baf2720b45c42f09b32e610a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Feb 20 12:02:47 2014 +0100
Add the template to add multiple candidates to an election
commit 9a01f6cde158b6fb1a1b68f40be33a89626de2dc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Feb 20 12:02:05 2014 +0100
Add form to add multiple candidates in one go
commit 4f51e60e9c1bfa5f3fe4ff07ee75f36791b90911
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:57:26 2014 +0100
Update the template used as front page of the admin section
commit 638ce919abe5a4b332e2044edc456afaccc3eb36
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:44:37 2014 +0100
Add a little text to the front page of elections
commit 0b57092b04ce971f7669d0f16e088eb6119c3a84
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:37:47 2014 +0100
Remove the frontpage boolean/checkbox from the form as we no longer use it
commit d715cde71ef0f06dfb6e59af79f9be9bfa7ed1cc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:37:28 2014 +0100
Small css fix to give a little space around the tables
commit 2c9719c0c4322fb6a6f4a0887f2a6c4504e751f1
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:37:16 2014 +0100
Change the front page to display some information about the current, future and past elections
commit 501f9e3fd5d41fba45778e3cff60dd592a38d3da
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:34:57 2014 +0100
Add tags to display the menu correctly
commit a287d95048914a3e20ad7a184ad557c3234a7fb4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 18:32:40 2014 +0100
Fix the Election.get_open_election method and add Election.get_next_election
commit 55f70d5fd973d5c095f2eac72eabfc8d2ab31c48
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:57:26 2014 +0100
Update the template used as front page of the admin section
commit d1045167275643df34016c080fd17dd681e53f7f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:18:55 2014 +0100
Rework the add election form
commit 2d184e38daa10bb1ba44e05ef59845e3cbcb2512
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:54:21 2014 +0100
Remove images we will no longer use
commit 37c1b47a73d2bb675f52ba4a4ab3adc0306d74f0
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:41:47 2014 +0100
CSS fixes: bring back the coloring when validating a vote - remove unused css
commit 64094d8701ca528348ef3021ad8096b2812d9b0a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:18:55 2014 +0100
Rework the add election form
commit 1fb2c18306b4ed62c84370c6b88bb51c8fba87e2
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:14:45 2014 +0100
Fix the page for adding a new candidate
commit d8c46f4f2f9512f8ae815e294779cd15d027a464
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:57:50 2014 +0100
Add dedicated macro to present the fields of a form in row
commit 13b2d33852a8c1abbce09707511ff5620a2b1e51
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:57:26 2014 +0100
Update the template used as front page of the admin section
commit 0425984ffa3aee79ae22a25f59e594061a047919
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:57:10 2014 +0100
Inform the user if we have no election on the front page
commit 37ceb8138d6e8530df2c9ac54ef67cc77206ef4c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:56:56 2014 +0100
Update the base template to the new theme
commit 110175c808f69533993e88aa470501b35769fc3e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:55:58 2014 +0100
Add the koji.css our base and the elections.css dedicated to elections
commit 23c28ff91f2bb32bb463a0d627e7313b3364a476
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:55:34 2014 +0100
Add jQuery CSS
commit 047ffaff7e27d5ffa425a1f9ffe19ea1180039e7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:55:25 2014 +0100
Add jQuery and our jQuery UI
commit fbe55d27da1c3aff8835b1a5d752c9c6fbb05fda
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:55:06 2014 +0100
Add the images used by jQuery
commit d23baa6b061285b2a0d060e8cdb72f200e7af792
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:54:51 2014 +0100
Add the election logo
commit 2bf87f1fb95042c34d4492cc036ea7c8b1f80b09
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:54:43 2014 +0100
If there is no elections, keep going this will be our admin index page
commit 4de57bfe2e5e0988012c85894382cb7c02471b2f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:29:24 2014 +0100
Inject some variables into every templates
commit ea37f37c03bdcdd98918e98b160e8baaccdbc122
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:29:02 2014 +0100
Add a top level __version__ and use this in the setup.py
commit 8f48bff2a5e156b0f6931436c680327103379ca4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:26:35 2014 +0100
Create a dedicated function to check is the user is an admin
commit 60d815402da6e3afb0453f98fe1b096b20fa7d0a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:20:39 2014 +0100
Fix link, we have auth_login not login here
commit a3ee3e9af19d09a12d210da78ff1a313895af4a4
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:16:53 2014 +0100
Add some missing dependencies
commit 6511bc3125ddc430194991546e384d90d0e55073
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:58:51 2014 +0100
Remove the login template, no longer used
commit 3b3a867e36267b7d23b0669ec8b838f30e94f944
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:58:15 2014 +0100
Little clean up in the master template
commit 2b8661c5c598fd82b6dfd2727672846316a32d44
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:58:05 2014 +0100
Remove the login form, no longer needed
commit 595400ca082ebb5817c4b8d6b9dd4e68d5c2bb36
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:57:55 2014 +0100
Adjust the requirements to list python-fedora and not flask-fas
commit 8cd0927db1a7c635e40464b11119a6c6bfb04625
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:57:18 2014 +0100
Port from flask-fas to flask-fas-openid
commit 7eff0fad00f887849bbb1ace4e93baee893b899b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sun Mar 9 20:53:59 2014 +0100
Style change
Let's be consistent throughout the application on how we write the render_template
arguments.
commit 7afbaa483254f3630ba2a0e3548ad211357ec918
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Sat Mar 1 07:49:39 2014 +0100
pep8 fixes
commit ec80874fa30e3eb1ccf4c0c0686e3d53fa6d4bb7
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Feb 28 10:04:14 2014 +0100
Reduce indentation
commit bbf92c0704906c10c34a58fcb5541b16ce03b345
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Thu Feb 20 11:21:51 2014 +0100
Adjust the runserver script
commit 506b9ba72a1af02a6827220f85dfdc5286891789
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:40:17 2014 +0100
Fix typo when retrieving a valid election
commit c0743d1b9fa61aed0b289b17b59ea15654d12550
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 17:12:53 2014 +0100
Fix retrieving a valid election that have ended
commit 554bdfc86ee059dc7324003eca81be8d2dbda84f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 16:59:05 2014 +0100
Bug fix in Vote.of_user_on_election() method to account for the count argument
commit 220530c96547b0b2e9b1c56c72a33253c4b2f5e6
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:51:54 2014 +0100
Adjust requirements
commit ae1a72a41a0a4e6d72022eff1cbd43a59c4007fc
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:51:47 2014 +0100
Add decorator when login is required
commit 51056537b3856b5cf548a40d69fd9762c0f00734
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:13:25 2014 +0100
Add script to easily create the database required to work on elections
commit 207e076c93dd5fbbace55a74bc3a191f53e5cb88
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:12:59 2014 +0100
Adjust the tests to run on the current application
commit 1e89858e47c18df3cfbff3ac1ccf5b15c867ee6e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:12:32 2014 +0100
We no longer have a development configuration file to use on the tests
commit b7947e2a95afb668aeebb97f16c2f5f4104faf59
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:12:10 2014 +0100
Port the flask application away from flask-sqlalchemy
This allows us to have all the querying done in one place
and not tight coupling of the logic with this library.
commit 5c341fe8befe02aef39af14705c88c51e35b195d
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:09:38 2014 +0100
Rework the models.py file to port it to plain SQLAlchemy
Expend the classes as well so that all the querying is done
only in this file an nowhere else in the project
commit 6febe17985964f8bb701bc618271469a9516eec5
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:08:42 2014 +0100
We cannot have 0 elections, this is taken care of in the controller
commit d9efe14cf52ecc6c4e1e93306ddf2f5274ec7a7c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:08:39 2014 +0100
We cannot have 0 elections, this is taken care of in the controller
commit ea25ce64260fb52502f738cfb4043a5f20305d7a
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:08:32 2014 +0100
We cannot have 0 elections, this is taken care of in the controller
commit 1e4a550e077d8eb53ef0f163f0322e6390b0545f
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 15:08:15 2014 +0100
Remove queries from the form
commit 0476d1e745c21aaa97e57faf08e0454f4351fc7e
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 13:22:49 2014 +0100
Remove un-necessary configuraiton file now that everything is moved to the default_config
commit 01651e69b77d541f417f00cbc6ebcc8942cffa39
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 13:21:54 2014 +0100
Populate the default config with the values present in the sample development configuration file
commit 9f64bdb0dea8864e01242669d2ecef70d79def3b
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 13:21:20 2014 +0100
Remove the sample createdb.in and the corresponding python script
commit cd8aa532e7f29b74d24b9834864bd849637a63ce
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 13:07:48 2014 +0100
Do not run on port 80 by default as this will conflict with any apache server running
commit ba2fac78417b0b8723a68e3cb9c8f519d5d0deab
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Wed Feb 19 13:01:49 2014 +0100
Fix the forms to use directly wtforms instead of flask-wtf
There has been a release of flask-wtf that stopped providing the interface to wtforms.
This commit allows us to deal with these changes in flask-wtf
commit de346f20a8cb8843ca018d046fefdee45f7e8fc6
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:27:14 2013 +0000
Updates for new version
commit ef352d1d54b450f4c0ba995c9fed732a20710746
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:26:17 2013 +0000
Update directory path
commit fae9a3a58f1c25746fd9173ecceaffe13a6607a7
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:25:36 2013 +0000
Update directory path
commit a1521f7554383691b437d3e3ce6472372c8dd77c
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:24:36 2013 +0000
Update version
commit 189416af2fd33008bdf7ffe04ab55fb8246669c1
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:24:12 2013 +0000
Update task list
commit e8e99be24b1218f8cb37e67265add3fb7eb02b74
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Oct 20 15:23:57 2013 +0000
Add info to clone git repo
commit bb897417b80b7da01d03b41f2ce307e01de612dd
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Mon Sep 2 21:00:46 2013 +0000
More simple voting updates
commit 66215d90cf21693683e214d233bf6234f7ea33c6
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sun Aug 25 20:21:10 2013 +0000
Create simple vote election
commit a466eab8f9148a04b43d2bef100c07166190dea1
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Aug 24 03:38:28 2013 +0000
Restructure.
commit b7a1bd9b75436fa43cf9f0a79778c4801fa3dcea
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Aug 24 03:33:26 2013 +0000
Update instructions
commit 9a6911ea399d806b49253d2f3172f7cfacc9853a
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Thu Jul 25 03:20:53 2013 +0000
Add constraint to votes table
commit e7680eacb9dadb5ee7fa92e8bc1dfce4c9d879ae
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Thu Jul 25 03:20:23 2013 +0000
Fix processing 'My Elections'
commit 3629c4879b81045096ff3d9f9b322b9eaf232d0f
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jul 6 03:14:04 2013 +0000
Add task
commit 8ebe96391625cd52f56ea7626a4ff5798cb96924
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jul 6 03:13:25 2013 +0000
Fix db issues
commit 050d76f41d00f1cf9e229389d65be83793f72c0d
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jul 6 03:12:38 2013 +0000
Specify dummy FAS account
commit 586dbecada6b455db9dbc735c4ce7067623dd587
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jul 6 03:11:26 2013 +0000
Add elections owner to db
commit 3df48265145928d0abcfacdf8040c39542ab46c6
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jul 6 03:10:44 2013 +0000
Fix no elections issue
commit 6892cc5876dfe28af1a895e9340ca367b59b4531
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Sat Jun 15 03:56:56 2013 +0000
Changes to support building a good package
commit 4cf7c3d1603bc45393f587deafa83db2323bfd7e
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Thu May 9 03:52:52 2013 +0000
Change for RPM
commit 329206a96987111f6b0499348fb2eb3e3d55e12e
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Thu May 9 03:52:20 2013 +0000
Change for RPM
commit 65012deb9bce238fbcb2c6eb9aa24d76edc6aaf5
Author: Frank Chiulli <fchiulli(a)fedoraproject.org>
Date: Tue Apr 30 04:02:50 2013 +0000
httpd/wsgi now working
commit f56a328a757e21391e73f6e9fd1d869cfd06b846
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Wed Feb 27 23:29:37 2013 -0500
Correct version
commit 8aa5f96d95984632567a9f647ddbf17475c8bbab
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Wed Feb 27 22:35:59 2013 -0500
Update documentation
commit 3e9c2417f2b034b4a738247881fffd622967758a
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Wed Feb 27 22:35:32 2013 -0500
Fix AccountSystem call
commit ccf3ea666e5824789b04cfd8d236435cf32f49ab
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Sun Feb 17 23:01:56 2013 -0500
Test again
commit c2db136edf3a384fc45f33b8347e5535aeee5ebb
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Sun Feb 17 22:49:05 2013 -0500
Test git push
commit 164886f12c723c6e74f9352068892ed55c3fc074
Author: fchiulli <fchiulli(a)elections-dev-i-57734127.(none)>
Date: Sun Feb 17 19:05:01 2013 -0500
Lots of updates/changes
commit bbe83e8422a520587ddd902426b1d8a5c4be1b03
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 21:40:58 2012 -0400
Make fedmsg optional with a shim module.
commit be286a4df4ec863cc68d05be8fdf60817428b238
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 21:28:21 2012 -0400
Revert "Add fedmsg to the list of requirements."
This reverts commit 471fabeecb4121b7837b7de509cee76385cfe8f8.
commit d5e0024f1f3b4508d861372b220239546db83085
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 12:09:59 2012 -0400
Emit a fedmsg message when a new election is created.
commit 050ec3cdacf936b5cbfe98d0a81ff789db30df00
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 12:09:38 2012 -0400
Add fedmsg to the list of requirements.
commit 1941a7d90de44f90a89ee3a4d56a40049ed91d0a
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 12:09:20 2012 -0400
Add a fedmsg development config.
commit 4a4cb5f62aad2f6e389dd3be9d7edcbca3166809
Author: Ralph Bean <rbean(a)redhat.com>
Date: Fri Aug 31 12:08:52 2012 -0400
Move kitchen above python-fedora in the requirements.txt list.
commit f82ba41deac98f6454a31e6b87478ee07e2e07bb
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Tue Aug 28 08:35:35 2012 -0700
Rest of the changes to merge about into vote
commit 1a5b559e9d86a2b000ff0c0828b7216d6df22ebd
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Tue Aug 28 08:34:47 2012 -0700
Merge the about page into the vote page
commit dddeae455d73097d0709a497a12257f1089e1071
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 18:47:41 2012 -0700
Some more pages and functions that are blockers
commit 9030f25ac95ebb3fb4a5967d45751a892e184313
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 18:44:59 2012 -0700
Add an implementation of a page about an election
commit 1a9a48b01a5506e1269e4ebada4a0f2145eb7b25
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 18:42:28 2012 -0700
Add two new pages for dsmoketesting
commit 7c9c373bc25d159cd8c79ceba46eacc657cf904b
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 18:23:22 2012 -0700
A few more todo items: i18n, more paranoid error handling
commit 0778412e3e43f6d761626348ca5e491cc94613ed
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 17:43:22 2012 -0700
Smoketesting now covers all urls' GET method.
commit 831ca1ca44f7fa6769ce684abe52f29bff05ce22
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 15:28:54 2012 -0700
Add ability to specify site-specifc css
commit f147339fc8504ec7eb6c162b7c8a2f5927d999d9
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 15:18:49 2012 -0700
Fix traceback for hitting logout when the user is not logged in
commit 9a59a33252d3c546ce543f1475327d13d40b1136
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 15:18:14 2012 -0700
Add unittests -- currently just a few smoketests
commit 4e5d18c1df3b2e7aa423307879aad3f53f91725d
Author: Toshio Kuratomi <toshio(a)fedoraproject.org>
Date: Mon Aug 27 11:27:11 2012 -0700
Add a TODO file
commit ac55fdad53a38675dd7b9ca52ec35fe4d263418b
Author: Ralph Bean <rbean(a)redhat.com>
Date: Mon Aug 20 15:20:22 2012 -0400
Added preliminary requirements.txt.
commit bcae2a7153b8cdfcc4188ff33a77fc07fa24f716
Author: Ian Weller <ian(a)ianweller.org>
Date: Sun Jul 15 09:54:58 2012 -0500
List frontpaged elections on front page
commit 247fd649eacb58c9b132d3421a37b5b1e34c6603
Author: Ian Weller <ian(a)ianweller.org>
Date: Thu Jul 12 16:59:14 2012 -0500
Initial commit with mostly-working admin
commit 6e013f2a42075c561080cee07ec64d368e5e5e4c
Author: Pierre-Yves Chibon <pingou(a)pingoured.fr>
Date: Fri Jul 11 13:13:41 2014 +0200
Remove everything so that we can put the flask port on the top of this
9 years, 10 months