Update the nuancier playbook
- Remove hotfix which are now included in 0.1.2 - Let the playbook set the SELinux boolean since the dependency is now installed by role/base
This commit is contained in:
parent
0a63a867c0
commit
545d915207
3 changed files with 4 additions and 991 deletions
|
@ -59,39 +59,10 @@
|
|||
notify:
|
||||
- restart apache
|
||||
|
||||
# FIXME: Probably need other selinux booleans and tweaks here
|
||||
# seboolean action requires libsemanage-python to be installed. Have to decide
|
||||
# if we actually want to install that on the hosts.
|
||||
# selinux is currently set to permissive due to other things that nuancier is
|
||||
# trying to do. So no harm in leaving this disabled for now.
|
||||
#- name: set sebooleans so nuancier can talk to the db
|
||||
# action: seboolean name=httpd_can_network_connect_db
|
||||
# state=true
|
||||
# persistent=true
|
||||
|
||||
- name: hotfix nuancier to get an error message for people not in cla_plus_one
|
||||
template: src={{ item.file }}
|
||||
dest={{ item.location }}/{{ item.file }}
|
||||
owner=root group=root mode=0644
|
||||
with_items:
|
||||
- { file: __init__.py, location: /usr/lib/python2.6/site-packages/nuancier }
|
||||
tags:
|
||||
- config
|
||||
- hotfix
|
||||
notify:
|
||||
- restart apache
|
||||
|
||||
- name: hotfix nuancier to fix results page
|
||||
template: src={{ item.file }}
|
||||
dest={{ item.location }}/{{ item.file }}
|
||||
owner=root group=root mode=0644
|
||||
with_items:
|
||||
- { file: model.py, location: /usr/lib/python2.6/site-packages/nuancier/lib }
|
||||
tags:
|
||||
- config
|
||||
- hotfix
|
||||
notify:
|
||||
- restart apache
|
||||
- name: set sebooleans so nuancier can talk to the db
|
||||
action: seboolean name=httpd_can_network_connect_db
|
||||
state=true
|
||||
persistent=true
|
||||
|
||||
- name: hotfix python-fedora-flask to include latest flask_fas_openid
|
||||
template: src={{ item.file }}
|
||||
|
|
|
@ -1,581 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2013 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.
|
||||
#
|
||||
|
||||
'''
|
||||
Top level of the nuancier-lite Flask application.
|
||||
'''
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
|
||||
import flask
|
||||
import dogpile.cache
|
||||
from functools import wraps
|
||||
from flask.ext.fas_openid import FAS
|
||||
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
import forms
|
||||
import lib as nuancierlib
|
||||
import notifications
|
||||
|
||||
|
||||
__version__ = '0.1.1'
|
||||
|
||||
APP = flask.Flask(__name__)
|
||||
APP.config.from_object('nuancier.default_config')
|
||||
if 'NUANCIER_CONFIG' in os.environ: # pragma: no cover
|
||||
APP.config.from_envvar('NUANCIER_CONFIG')
|
||||
|
||||
# Set up FAS extension
|
||||
FAS = FAS(APP)
|
||||
|
||||
# Initialize the cache.
|
||||
CACHE = dogpile.cache.make_region().configure(
|
||||
APP.config.get('NUANCIER_CACHE_BACKEND', 'dogpile.cache.memory'),
|
||||
**APP.config.get('NUANCIER_CACHE_KWARGS', {})
|
||||
)
|
||||
|
||||
SESSION = nuancierlib.create_session(APP.config['DB_URL'])
|
||||
|
||||
|
||||
def is_nuancier_admin():
|
||||
""" Is the user a nuancier admin.
|
||||
"""
|
||||
if not hasattr(flask.g, 'fas_user') or not flask.g.fas_user:
|
||||
return False
|
||||
if not flask.g.fas_user.cla_done or \
|
||||
len(flask.g.fas_user.groups) < 1:
|
||||
return False
|
||||
|
||||
admins = APP.config['ADMIN_GROUP']
|
||||
if isinstance(admins, basestring):
|
||||
admins = set([admins])
|
||||
else:
|
||||
admins = set(admins)
|
||||
|
||||
return len(set(flask.g.fas_user.groups).intersection(admins)) > 0
|
||||
|
||||
|
||||
def fas_login_required(function):
|
||||
""" Flask decorator to ensure that the user is logged in against FAS.
|
||||
To use this decorator you need to have a function named 'auth_login'.
|
||||
Without that function the redirect if the user is not logged in will not
|
||||
work.
|
||||
|
||||
We'll always make sure the user is CLA+1 as it's what's needed to be
|
||||
allowed to vote.
|
||||
"""
|
||||
@wraps(function)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if flask.g.fas_user is None:
|
||||
return flask.redirect(flask.url_for(
|
||||
'.login', next=flask.request.url))
|
||||
if not flask.g.fas_user.cla_done \
|
||||
or len(flask.g.fas_user.groups) < 1:
|
||||
flask.flash('You need to have signed the FPCA and be in one non-CLA'
|
||||
' group to vote in this election', 'errors')
|
||||
return flask.redirect(flask.url_for('index'))
|
||||
return function(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
def nuancier_admin_required(function):
|
||||
""" Decorator used to check if the loged in user is a nuancier admin
|
||||
or not.
|
||||
"""
|
||||
@wraps(function)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if flask.g.fas_user is None:
|
||||
return flask.redirect(flask.url_for('.login',
|
||||
next=flask.request.url))
|
||||
elif not is_nuancier_admin():
|
||||
flask.flash('You are not an administrator of nuancier-lite',
|
||||
'errors')
|
||||
return flask.redirect(flask.url_for('index'))
|
||||
else:
|
||||
return function(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
## APP
|
||||
|
||||
@APP.context_processor
|
||||
def inject_is_admin():
|
||||
""" Inject whether the user is a nuancier admin or not in every page
|
||||
(every template).
|
||||
"""
|
||||
return dict(is_admin=is_nuancier_admin(),
|
||||
version=__version__)
|
||||
|
||||
|
||||
# pylint: disable=W0613
|
||||
@APP.teardown_request
|
||||
def shutdown_session(exception=None):
|
||||
""" Remove the DB session at the end of each request. """
|
||||
SESSION.remove()
|
||||
|
||||
|
||||
@APP.route('/msg/')
|
||||
def msg():
|
||||
""" Page used to display error messages
|
||||
"""
|
||||
return flask.render_template('msg.html')
|
||||
|
||||
|
||||
@APP.route('/login/', methods=['GET', 'POST'])
|
||||
def login():
|
||||
""" Login mechanism for this application.
|
||||
"""
|
||||
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 logout():
|
||||
""" Log out if the user is logged in other do nothing.
|
||||
Return to the index page at the end.
|
||||
"""
|
||||
if hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None:
|
||||
FAS.logout()
|
||||
return flask.redirect(flask.url_for('.index'))
|
||||
|
||||
|
||||
@CACHE.cache_on_arguments(expiration_time=3600)
|
||||
@APP.route('/pictures/<path:filename>')
|
||||
def base_picture(filename):
|
||||
return flask.send_from_directory(APP.config['PICTURE_FOLDER'], filename)
|
||||
|
||||
|
||||
@CACHE.cache_on_arguments(expiration_time=3600)
|
||||
@APP.route('/cache/<path:filename>')
|
||||
def base_cache(filename):
|
||||
return flask.send_from_directory(APP.config['CACHE_FOLDER'], filename)
|
||||
|
||||
|
||||
@APP.route('/')
|
||||
def index():
|
||||
''' Display the index page. '''
|
||||
elections = nuancierlib.get_elections_open(SESSION)
|
||||
return flask.render_template('index.html', elections=elections)
|
||||
|
||||
|
||||
@APP.route('/elections/')
|
||||
def elections_list():
|
||||
''' Displays the results of all published election. '''
|
||||
elections = nuancierlib.get_elections(SESSION)
|
||||
|
||||
return flask.render_template(
|
||||
'elections_list.html',
|
||||
elections=elections)
|
||||
|
||||
|
||||
@APP.route('/election/<int:election_id>/')
|
||||
def election(election_id):
|
||||
''' Display the index page of the election will all the candidates
|
||||
submitted. '''
|
||||
election = nuancierlib.get_election(SESSION, election_id)
|
||||
if not election:
|
||||
flask.flash('No election found', 'error')
|
||||
return flask.render_template('msg.html')
|
||||
|
||||
# How many votes the user made:
|
||||
votes = []
|
||||
can_vote = True
|
||||
if flask.g.fas_user:
|
||||
votes = nuancierlib.get_votes_user(SESSION, election_id,
|
||||
flask.g.fas_user.username)
|
||||
|
||||
if election.election_open and len(votes) < election.election_n_choice:
|
||||
if len(votes) > 0:
|
||||
flask.flash('You have already voted, but you can still vote '
|
||||
'on more candidates.')
|
||||
return flask.redirect(flask.url_for('vote', election_id=election_id))
|
||||
elif election.election_open and len(votes) >= election.election_n_choice:
|
||||
can_vote = False
|
||||
else:
|
||||
flask.flash('This election is not open', 'error')
|
||||
|
||||
candidates = nuancierlib.get_candidates(SESSION, election_id)
|
||||
|
||||
if flask.g.fas_user:
|
||||
random.seed(
|
||||
int(
|
||||
hashlib.sha1(flask.g.fas_user.username).hexdigest(), 16
|
||||
) % 100000)
|
||||
random.shuffle(candidates)
|
||||
|
||||
return flask.render_template(
|
||||
'election.html',
|
||||
candidates=candidates,
|
||||
election=election,
|
||||
can_vote=can_vote,
|
||||
picture_folder=os.path.join(
|
||||
APP.config['PICTURE_FOLDER'], election.election_folder),
|
||||
cache_folder=os.path.join(
|
||||
APP.config['CACHE_FOLDER'], election.election_folder)
|
||||
)
|
||||
|
||||
|
||||
@APP.route('/election/<int:election_id>/vote/')
|
||||
@fas_login_required
|
||||
def vote(election_id):
|
||||
''' Give the possibility to the user to vote for an election. '''
|
||||
election = nuancierlib.get_election(SESSION, election_id)
|
||||
if not election:
|
||||
flask.flash('No election found', 'error')
|
||||
return flask.render_template('msg.html')
|
||||
candidates = nuancierlib.get_candidates(SESSION, election_id)
|
||||
|
||||
if not election.election_open:
|
||||
flask.flash('This election is not open', 'error')
|
||||
return flask.redirect(flask.url_for('index'))
|
||||
|
||||
if flask.g.fas_user:
|
||||
random.seed(
|
||||
int(
|
||||
hashlib.sha1(flask.g.fas_user.username).hexdigest(), 16
|
||||
) % 100000)
|
||||
random.shuffle(candidates)
|
||||
|
||||
# How many votes the user made:
|
||||
votes = nuancierlib.get_votes_user(SESSION, election_id,
|
||||
flask.g.fas_user.username)
|
||||
|
||||
if len(votes) >= election.election_n_choice:
|
||||
flask.flash('You have cast the maximal number of votes '
|
||||
'allowed for this election.', 'error')
|
||||
return flask.redirect(
|
||||
flask.url_for('election', election_id=election_id))
|
||||
|
||||
if len(votes) > 0:
|
||||
candidate_done = [cdt.candidate_id for cdt in votes]
|
||||
candidates = [candidate
|
||||
for candidate in candidates
|
||||
if candidate.id not in candidate_done]
|
||||
|
||||
return flask.render_template(
|
||||
'vote.html',
|
||||
election=election,
|
||||
candidates=candidates,
|
||||
n_votes_done=len(votes),
|
||||
picture_folder=os.path.join(
|
||||
APP.config['PICTURE_FOLDER'], election.election_folder),
|
||||
cache_folder=os.path.join(
|
||||
APP.config['CACHE_FOLDER'], election.election_folder)
|
||||
)
|
||||
|
||||
|
||||
@APP.route('/election/<int:election_id>/voted/', methods=['GET', 'POST'])
|
||||
@fas_login_required
|
||||
def process_vote(election_id):
|
||||
election = nuancierlib.get_election(SESSION, election_id)
|
||||
if not election:
|
||||
flask.flash('No election found', 'error')
|
||||
return flask.render_template('msg.html')
|
||||
|
||||
if not election.election_open:
|
||||
flask.flash('This election is not open', 'error')
|
||||
return flask.render_template('msg.html')
|
||||
|
||||
candidates = nuancierlib.get_candidates(SESSION, election_id)
|
||||
candidate_ids = set([candidate.id for candidate in candidates])
|
||||
|
||||
entries = set([int(entry)
|
||||
for entry in flask.request.form.getlist('selection')])
|
||||
|
||||
# If not enough candidates selected
|
||||
if not entries:
|
||||
flask.flash('You did not select any candidate to vote for.', 'error')
|
||||
return flask.redirect(flask.url_for('vote', election_id=election_id))
|
||||
|
||||
# If vote on candidates from other elections
|
||||
if not set(entries).issubset(candidate_ids):
|
||||
flask.flash('The selection you have made contains element which are '
|
||||
'part of this election, please be careful.', 'error')
|
||||
return flask.redirect(flask.url_for('vote', election_id=election_id))
|
||||
|
||||
# How many votes the user made:
|
||||
votes = nuancierlib.get_votes_user(SESSION, election_id,
|
||||
flask.g.fas_user.username)
|
||||
|
||||
# Too many votes -> redirect
|
||||
if len(votes) >= election.election_n_choice:
|
||||
flask.flash('You have cast the maximal number of votes '
|
||||
'allowed for this election.', 'error')
|
||||
return flask.redirect(
|
||||
flask.url_for('election', election_id=election_id))
|
||||
|
||||
# Selected more candidates than allowed -> redirect
|
||||
if len(votes) + len(entries) > election.election_n_choice:
|
||||
flask.flash('You selected %s wallpapers while you are only allowed '
|
||||
'to select %s' % (
|
||||
len(entries),
|
||||
(election.election_n_choice - len(votes))),
|
||||
'error')
|
||||
return flask.render_template(
|
||||
'vote.html',
|
||||
election=election,
|
||||
candidates=[nuancierlib.get_candidate(SESSION, candidate_id)
|
||||
for candidate_id in entries],
|
||||
n_votes_done=len(votes),
|
||||
picture_folder=os.path.join(
|
||||
APP.config['PICTURE_FOLDER'], election.election_folder),
|
||||
cache_folder=os.path.join(
|
||||
APP.config['CACHE_FOLDER'], election.election_folder)
|
||||
)
|
||||
|
||||
# Allowed to vote, selection sufficient, choice confirmed: process
|
||||
try:
|
||||
for selection in entries:
|
||||
nuancierlib.add_vote(SESSION, selection,
|
||||
flask.g.fas_user.username)
|
||||
except nuancierlib.NuancierException, err:
|
||||
flask.flash(err.message, 'error')
|
||||
|
||||
try:
|
||||
SESSION.commit()
|
||||
except SQLAlchemyError as err:
|
||||
SESSION.rollback()
|
||||
print >> sys.stderr, "Error while proccessing the vote:", err
|
||||
flask.flash('An error occured while processing your votes, please '
|
||||
'report this to your lovely admin or see logs for '
|
||||
'more details', 'error')
|
||||
|
||||
flask.flash('Your vote has been recorded, thank you for voting on '
|
||||
'%s %s' % (election.election_name, election.election_year))
|
||||
|
||||
if election.election_badge_link:
|
||||
flask.flash('Do not forget to <a href="%s" target="_blank">claim your '
|
||||
'badge!</a>' % election.election_badge_link)
|
||||
return flask.redirect(flask.url_for('elections_list'))
|
||||
|
||||
|
||||
@APP.route('/results/')
|
||||
def results_list():
|
||||
''' Displays the results of all published election. '''
|
||||
elections = nuancierlib.get_elections_public(SESSION)
|
||||
|
||||
return flask.render_template(
|
||||
'result_list.html',
|
||||
elections=elections)
|
||||
|
||||
|
||||
@APP.route('/results/<int:election_id>/')
|
||||
def results(election_id):
|
||||
''' Displays the results of an election. '''
|
||||
election = nuancierlib.get_election(SESSION, election_id)
|
||||
|
||||
if not election:
|
||||
flask.flash('No election found', 'error')
|
||||
return flask.render_template('msg.html')
|
||||
|
||||
if not election.election_public:
|
||||
flask.flash('The results this election are not public yet', 'error')
|
||||
return flask.redirect(flask.url_for('results_list'))
|
||||
|
||||
results = nuancierlib.get_results(SESSION, election_id)
|
||||
|
||||
return flask.render_template(
|
||||
'results.html',
|
||||
election=election,
|
||||
results=results,
|
||||
picture_folder=os.path.join(
|
||||
APP.config['PICTURE_FOLDER'], election.election_folder),
|
||||
cache_folder=os.path.join(
|
||||
APP.config['CACHE_FOLDER'], election.election_folder))
|
||||
|
||||
|
||||
## ADMIN
|
||||
|
||||
|
||||
@APP.route('/admin/')
|
||||
@nuancier_admin_required
|
||||
def admin_index():
|
||||
''' Display the index page of the admin interface. '''
|
||||
elections = nuancierlib.get_elections(SESSION)
|
||||
return flask.render_template('admin_index.html', elections=elections)
|
||||
|
||||
|
||||
@APP.route('/admin/new/', methods=['GET', 'POST'])
|
||||
@nuancier_admin_required
|
||||
def admin_new():
|
||||
''' Create a new election. '''
|
||||
form = forms.AddElectionForm()
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
election = nuancierlib.add_election(
|
||||
SESSION,
|
||||
election_name=form.election_name.data,
|
||||
election_folder=form.election_folder.data,
|
||||
election_year=form.election_year.data,
|
||||
election_open=form.election_open.data,
|
||||
election_n_choice=form.election_n_choice.data,
|
||||
election_badge_link=form.election_badge_link.data,
|
||||
)
|
||||
except nuancierlib.NuancierException as err:
|
||||
flask.flash(err.message, 'error')
|
||||
try:
|
||||
SESSION.commit()
|
||||
except SQLAlchemyError as err:
|
||||
SESSION.rollback()
|
||||
print >> sys.stderr, "Cannot create new election", err
|
||||
flask.flash(err.message, 'error')
|
||||
if form.generate_cache.data:
|
||||
return admin_cache(election.id)
|
||||
return flask.redirect(flask.url_for('admin_index'))
|
||||
return flask.render_template('admin_new.html', form=form)
|
||||
|
||||
|
||||
@APP.route('/admin/open/<int:election_id>')
|
||||
@nuancier_admin_required
|
||||
def admin_open(election_id):
|
||||
''' Flip the open state '''
|
||||
election = nuancierlib.get_election(SESSION, election_id)
|
||||
state = nuancierlib.toggle_open(SESSION, election_id)
|
||||
|
||||
if state:
|
||||
msg = "Election opened"
|
||||
else:
|
||||
msg = "Election ended"
|
||||
|
||||
try:
|
||||
SESSION.commit()
|
||||
except SQLAlchemyError as err:
|
||||
SESSION.rollback()
|
||||
print >> sys.stderr, "Cannot flip the open state", err
|
||||
flask.flash(err.message, 'error')
|
||||
else:
|
||||
flask.flash(msg)
|
||||
|
||||
if state:
|
||||
topic = "open.toggle.on"
|
||||
else:
|
||||
topic = "open.toggle.off"
|
||||
|
||||
notifications.publish(
|
||||
topic=topic,
|
||||
msg=dict(
|
||||
agent=flask.g.fas_user.username,
|
||||
election=election.api_repr(version=1),
|
||||
state=state,
|
||||
)
|
||||
)
|
||||
|
||||
return flask.redirect(flask.url_for('.admin_index'))
|
||||
|
||||
|
||||
@APP.route('/admin/publish/<int:election_id>')
|
||||
@nuancier_admin_required
|
||||
def admin_publish(election_id):
|
||||
''' Flip the public state '''
|
||||
election = nuancierlib.get_election(SESSION, election_id)
|
||||
state = nuancierlib.toggle_public(SESSION, election_id)
|
||||
|
||||
if state:
|
||||
msg = "Election published"
|
||||
else:
|
||||
msg = "Election closed"
|
||||
|
||||
try:
|
||||
SESSION.commit()
|
||||
except SQLAlchemyError as err:
|
||||
SESSION.rollback()
|
||||
print >> sys.stderr, "Cannot flip the publish state", err
|
||||
flask.flash(err.message, 'error')
|
||||
else:
|
||||
flask.flash(msg)
|
||||
|
||||
if state:
|
||||
topic = "publish.toggle.on"
|
||||
else:
|
||||
topic = "publish.toggle.off"
|
||||
|
||||
notifications.publish(
|
||||
topic=topic,
|
||||
msg=dict(
|
||||
agent=flask.g.fas_user.username,
|
||||
election=election.api_repr(version=1),
|
||||
state=state,
|
||||
)
|
||||
)
|
||||
|
||||
return flask.redirect(flask.url_for('.admin_index'))
|
||||
|
||||
|
||||
@APP.route('/admin/cache/<int:election_id>')
|
||||
@nuancier_admin_required
|
||||
def admin_cache(election_id):
|
||||
''' Regenerate the cache for this election. '''
|
||||
election = nuancierlib.get_election(SESSION, election_id)
|
||||
|
||||
if not election:
|
||||
flask.flash('No election found', 'error')
|
||||
return flask.render_template('msg.html')
|
||||
|
||||
try:
|
||||
nuancierlib.generate_cache(
|
||||
session=SESSION,
|
||||
election=election,
|
||||
picture_folder=APP.config['PICTURE_FOLDER'],
|
||||
cache_folder=APP.config['CACHE_FOLDER'],
|
||||
size=APP.config['THUMB_SIZE'])
|
||||
flask.flash('Cache regenerated for election %s' %
|
||||
election.election_name)
|
||||
except nuancierlib.NuancierException as err:
|
||||
SESSION.rollback()
|
||||
print >> sys.stderr, "Cannot generate cache", err
|
||||
flask.flash(err.message, 'error')
|
||||
|
||||
return flask.redirect(flask.url_for('.admin_index'))
|
||||
|
||||
|
||||
@APP.route('/admin/stats/<int:election_id>/')
|
||||
@nuancier_admin_required
|
||||
def stats(election_id):
|
||||
''' Return some stats about this election. '''
|
||||
election = nuancierlib.get_election(SESSION, election_id)
|
||||
|
||||
if not election:
|
||||
flask.flash('No election found', 'error')
|
||||
return flask.render_template('msg.html')
|
||||
|
||||
if not election.election_public:
|
||||
flask.flash('The results this election are not public yet', 'error')
|
||||
return flask.redirect(flask.url_for('results_list'))
|
||||
|
||||
stats = nuancierlib.get_stats(SESSION, election_id)
|
||||
|
||||
return flask.render_template(
|
||||
'stats.html',
|
||||
stats=stats,
|
||||
election=election)
|
|
@ -1,377 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2013 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.
|
||||
#
|
||||
|
||||
'''
|
||||
Mapping of python classes to Database Tables.
|
||||
'''
|
||||
|
||||
__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
|
||||
import pkg_resources
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.orm import scoped_session
|
||||
from sqlalchemy.orm import relation
|
||||
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()
|
||||
|
||||
error_log = logging.getLogger('nuancier.lib.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)
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
class Elections(BASE):
|
||||
'''This table lists all the elections available.
|
||||
|
||||
Table -- Elections
|
||||
'''
|
||||
|
||||
__tablename__ = 'Elections'
|
||||
id = sa.Column(sa.Integer, nullable=False, primary_key=True)
|
||||
election_name = sa.Column(sa.String(255), nullable=False, unique=True)
|
||||
election_folder = sa.Column(sa.String(50), nullable=False, unique=True)
|
||||
election_year = sa.Column(sa.Integer, nullable=False)
|
||||
election_open = sa.Column(sa.Boolean, nullable=False, default=False)
|
||||
election_public = sa.Column(sa.Boolean, nullable=False, default=False)
|
||||
election_n_choice = sa.Column(sa.Integer, nullable=False)
|
||||
election_badge_link = sa.Column(sa.String(255), default=None)
|
||||
|
||||
date_created = sa.Column(sa.DateTime, nullable=False,
|
||||
default=sa.func.current_timestamp())
|
||||
date_updated = sa.Column(sa.DateTime, nullable=False,
|
||||
default=sa.func.current_timestamp(),
|
||||
onupdate=sa.func.current_timestamp())
|
||||
|
||||
def __init__(self, election_name, election_folder, election_year,
|
||||
election_open=False, election_public=False,
|
||||
election_n_choice=16, election_badge_link=None):
|
||||
""" Constructor.
|
||||
|
||||
:arg election_name:
|
||||
:arg election_folder:
|
||||
:arg election_year:
|
||||
:arg election_open:
|
||||
:arg election_public:
|
||||
:arg election_n_choice:
|
||||
:arg election_badge_link:
|
||||
"""
|
||||
self.election_name = election_name
|
||||
self.election_folder = election_folder
|
||||
self.election_year = election_year
|
||||
self.election_open = election_open
|
||||
self.election_public = election_public
|
||||
self.election_n_choice = election_n_choice
|
||||
self.election_badge_link = election_badge_link
|
||||
|
||||
def __repr__(self):
|
||||
return 'Elections(id:%r, name:%r, year:%r)' % (
|
||||
self.id, self.election_name, self.election_year)
|
||||
|
||||
def api_repr(self, version):
|
||||
""" Used by fedmsg to serialize Elections in messages. """
|
||||
if version == 1:
|
||||
return dict(
|
||||
id=self.id,
|
||||
name=self.election_name,
|
||||
year=self.election_year,
|
||||
)
|
||||
else: # pragma: no cover
|
||||
raise NotImplementedError("Unsupported version %r" % version)
|
||||
|
||||
@classmethod
|
||||
def all(cls, session):
|
||||
""" Return all the entries in the elections table.
|
||||
"""
|
||||
return session.query(
|
||||
cls
|
||||
).order_by(
|
||||
Elections.election_year.desc()
|
||||
).all()
|
||||
|
||||
@classmethod
|
||||
def by_id(cls, session, election_id):
|
||||
""" Return the election corresponding to the provided identifier.
|
||||
"""
|
||||
return session.query(cls).get(election_id)
|
||||
|
||||
@classmethod
|
||||
def get_open(cls, session):
|
||||
""" Return all the election open.
|
||||
"""
|
||||
return session.query(
|
||||
cls
|
||||
).filter(
|
||||
Elections.election_open == True
|
||||
).order_by(
|
||||
Elections.election_year.desc()
|
||||
).all()
|
||||
|
||||
@classmethod
|
||||
def get_public(cls, session):
|
||||
""" Return all the election public.
|
||||
"""
|
||||
return session.query(
|
||||
cls
|
||||
).filter(
|
||||
Elections.election_public == True
|
||||
).order_by(
|
||||
Elections.election_year.desc()
|
||||
).all()
|
||||
|
||||
|
||||
class Candidates(BASE):
|
||||
''' This table lists the candidates for the elections.
|
||||
|
||||
Table -- Candidates
|
||||
'''
|
||||
|
||||
__tablename__ = 'Candidates'
|
||||
id = sa.Column(sa.Integer, nullable=False, primary_key=True)
|
||||
candidate_file = sa.Column(sa.String(255), nullable=False)
|
||||
candidate_name = sa.Column(sa.String(255), nullable=False)
|
||||
candidate_author = sa.Column(sa.String(255), nullable=False)
|
||||
election_id = sa.Column(
|
||||
sa.Integer,
|
||||
sa.ForeignKey('Elections.id',
|
||||
ondelete='CASCADE',
|
||||
onupdate='CASCADE'
|
||||
),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
date_created = sa.Column(sa.DateTime, nullable=False,
|
||||
default=sa.func.current_timestamp())
|
||||
date_updated = sa.Column(sa.DateTime, nullable=False,
|
||||
default=sa.func.current_timestamp(),
|
||||
onupdate=sa.func.current_timestamp())
|
||||
|
||||
election = relation('Elections')
|
||||
__table_args__ = (
|
||||
sa.UniqueConstraint('election_id', 'candidate_file'),
|
||||
sa.UniqueConstraint('election_id', 'candidate_name'),
|
||||
)
|
||||
|
||||
def __init__(self, candidate_file, candidate_name, candidate_author,
|
||||
election_id):
|
||||
""" Constructor
|
||||
|
||||
:arg candidate_file: the file name of the candidate
|
||||
:arg candidate_name: the name of the candidate
|
||||
:arg candidate_author: the name of the author of this candidate
|
||||
:arg election_id: the identifier of the election this candidate is
|
||||
candidate for.
|
||||
"""
|
||||
self.candidate_file = candidate_file
|
||||
self.candidate_name = candidate_name
|
||||
self.candidate_author = candidate_author
|
||||
self.election_id = election_id
|
||||
|
||||
def __repr__(self):
|
||||
return 'Candidates(file:%r, name:%r, election_id:%r, created:%r' % (
|
||||
self.candidate_file, self.candidate_name, self.election_id,
|
||||
self.date_created)
|
||||
|
||||
def api_repr(self, version):
|
||||
""" Used by fedmsg to serialize Packages in messages. """
|
||||
if version == 1:
|
||||
return dict(
|
||||
name=self.candidate_name,
|
||||
election=self.election.election_name,
|
||||
)
|
||||
else: # pragma: no cover
|
||||
raise NotImplementedError("Unsupported version %r" % version)
|
||||
|
||||
@classmethod
|
||||
def by_id(cls, session, candidate_id):
|
||||
""" Return the election corresponding to the provided identifier.
|
||||
"""
|
||||
return session.query(cls).get(candidate_id)
|
||||
|
||||
@classmethod
|
||||
def by_election(cls, session, election_id):
|
||||
""" Return the candidate associated to the given election
|
||||
identifier.
|
||||
|
||||
"""
|
||||
return session.query(cls).filter(
|
||||
Candidates.election_id == election_id).all()
|
||||
|
||||
@classmethod
|
||||
def get_results(cls, session, election_id):
|
||||
""" Return the candidate of a given election ranked by the number
|
||||
of vote each received.
|
||||
|
||||
"""
|
||||
query = session.query(
|
||||
Candidates,
|
||||
sa.func.count(Votes.candidate_id).label('votes')
|
||||
).filter(
|
||||
Candidates.election_id == election_id
|
||||
).filter(
|
||||
Candidates.id == Votes.candidate_id
|
||||
).group_by(
|
||||
Candidates.id, Candidates.candidate_file, Candidates.candidate_name, Candidates.candidate_author, Candidates.election_id, Candidates.date_created, Candidates.date_updated
|
||||
).order_by(
|
||||
'votes DESC'
|
||||
)
|
||||
return query.all()
|
||||
|
||||
|
||||
class Votes(BASE):
|
||||
''' This table lists the results of the elections
|
||||
|
||||
Table -- Votes
|
||||
'''
|
||||
|
||||
__tablename__ = 'Votes'
|
||||
user_name = sa.Column(sa.String(50), nullable=False, primary_key=True)
|
||||
candidate_id = sa.Column(
|
||||
sa.Integer,
|
||||
sa.ForeignKey('Candidates.id',
|
||||
onupdate='CASCADE'
|
||||
),
|
||||
nullable=False,
|
||||
primary_key=True
|
||||
)
|
||||
|
||||
date_created = sa.Column(sa.DateTime, nullable=False,
|
||||
default=sa.func.current_timestamp())
|
||||
date_updated = sa.Column(sa.DateTime, nullable=False,
|
||||
default=sa.func.current_timestamp(),
|
||||
onupdate=sa.func.current_timestamp())
|
||||
|
||||
def __init__(self, user_name, candidate_id):
|
||||
""" Constructor
|
||||
|
||||
:arg name: the name of the user who voted
|
||||
:arg candidate_id: the identifier of the candidate that the user
|
||||
voted for.
|
||||
"""
|
||||
self.user_name = user_name
|
||||
self.candidate_id = candidate_id
|
||||
|
||||
def __repr__(self):
|
||||
return 'Votes(name:%r, candidate_id:%r, created:%r' % (
|
||||
self.user_name, self.candidate_id, self.date_created)
|
||||
|
||||
@classmethod
|
||||
def cnt_votes(cls, session, election_id,):
|
||||
""" Return the votes on the specified election.
|
||||
|
||||
:arg session:
|
||||
:arg election_id:
|
||||
"""
|
||||
return session.query(
|
||||
cls
|
||||
).filter(
|
||||
Votes.candidate_id == Candidates.id
|
||||
).filter(
|
||||
Candidates.election_id == election_id
|
||||
).count()
|
||||
|
||||
@classmethod
|
||||
def cnt_voters(cls, session, election_id,):
|
||||
""" Return the votes on the specified election.
|
||||
|
||||
:arg session:
|
||||
:arg election_id:
|
||||
"""
|
||||
return session.query(
|
||||
sa.func.distinct(cls.user_name)
|
||||
).filter(
|
||||
Votes.candidate_id == Candidates.id
|
||||
).filter(
|
||||
Candidates.election_id == election_id
|
||||
).count()
|
||||
|
||||
@classmethod
|
||||
def by_election(cls, session, election_id):
|
||||
""" Return the votes on the specified election.
|
||||
|
||||
:arg session:
|
||||
:arg election_id:
|
||||
"""
|
||||
return session.query(
|
||||
cls
|
||||
).filter(
|
||||
Votes.candidate_id == Candidates.id
|
||||
).filter(
|
||||
Candidates.election_id == election_id
|
||||
).all()
|
||||
|
||||
@classmethod
|
||||
def by_election_user(cls, session, election_id, username):
|
||||
""" Return the votes the specified user casted on the specified
|
||||
election.
|
||||
|
||||
:arg session:
|
||||
:arg election_id:
|
||||
:arg username:
|
||||
"""
|
||||
return session.query(
|
||||
cls
|
||||
).filter(
|
||||
Votes.candidate_id == Candidates.id
|
||||
).filter(
|
||||
Candidates.election_id == election_id
|
||||
).filter(
|
||||
Votes.user_name == username
|
||||
).all()
|
Loading…
Add table
Add a link
Reference in a new issue