.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')
-
-
-@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')
-
-
-@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')
-
-
-@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')
+
+
+@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')
+
+
+@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')
+
+
+@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')
-@APP.route('/admin/<election_alias>/candidates/new/multi',
methods=('GET', 'POST'))
+@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')
+@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...
- </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,"(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')
-@app.route('/admin/<election_alias>/candidates/<int:candidate_id>/edit',
+
+@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')
-@app.route('/admin/<election_alias>/candidates/<int:candidate_id>/delete',
+@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')
-
-
-@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')
-
-
-@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...
- </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')
+
+
+@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')
+
+
+@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...
+ </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')
+
+
+@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')
+
+
+@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...
+ </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')
-
-
-@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')
-
-
-@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...
- </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...
+ </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')
+
+
+@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')
+
+
+@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"&...
Password?')}</a></li>
- <li><a
href="https://admin.fedoraproject.org/accounts/user/new">${_...
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 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>&l...
- <li><a
href="http://fedoraproject.org/wiki/Legal/TrademarkGuidelines"&...
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.