wcs/extra/pyvotecore/vote_field.py

278 lines
11 KiB
Python

# w.c.s. - web application for online forms
# Copyright (C) 2005-2010 Entr'ouvert
#
# 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
import random
from quixote.html import htmltext, TemplateIO
from qommon.form import CompositeWidget, IntWidget, WidgetList, StringWidget, \
CheckboxWidget, SingleSelectWidget
from wcs.fields import WidgetField, register_field_class
from pyvotecore import schulze_method, irv, ranked_pairs, schulze_pr, \
schulze_stv, schulze_npr
class VoteWidget(CompositeWidget):
readonly = False
def __init__(self, name, value = None, elements = None, **kwargs):
CompositeWidget.__init__(self, name, value, **kwargs)
self.element_names = {}
if kwargs.has_key('title'):
del kwargs['title']
if kwargs.has_key('readonly'):
if kwargs['readonly']:
self.readonly = True
del kwargs['readonly']
if kwargs.has_key('required'):
if kwargs['required']:
self.required = True
del kwargs['required']
self.randomize_items = False
if kwargs.has_key('randomize_items'):
if kwargs['randomize_items']:
self.randomize_items = True
del kwargs['randomize_items']
for v in elements:
if type(v) is tuple:
title = v[1]
key = v[0]
if type(key) is int:
name = 'element%d' % v[0]
elif type(key) in (str, htmltext):
name = str('element%s' % v[0])
key = str(key)
else:
raise NotImplementedError()
else:
title = v
key = v
name = 'element%d' % len(self.element_names.keys())
if value:
position = value.get(key)
else:
position = None
self.add(IntWidget, name, title = title, value = position, size = 5, **kwargs)
self.element_names[name] = key
if self.randomize_items:
random.shuffle(self.widgets)
if self.readonly:
def cmp_w(x, y):
if x.value is None and y.value is None:
return 0
if x.value is None:
return 1
if y.value is None:
return -1
return cmp(x.value, y.value)
self.widgets.sort(cmp_w)
def _parse(self, request):
values = {}
for name in self.element_names:
value = self.get(name)
values[self.element_names[name]] = value
if type(value) is not int:
self.get_widget(name).set_error(IntWidget.TYPE_ERROR)
self.value = values or None
def parse(self, request=None):
value = CompositeWidget.parse(self, request=request)
for widget in self.widgets:
if widget.has_error():
self.set_error(_('Some fields were not filled properly.'))
break
return value
def render_content(self):
r = TemplateIO(html=True)
r += htmltext('<ul>')
for widget in self.get_widgets():
if widget.has_error():
r += htmltext('<li class="error"><label>')
else:
r += htmltext('<li><label>')
if self.readonly:
widget.attrs['disabled'] = 'disabled'
if widget.value:
r += htmltext('<input type="hidden" name="%s" value="%s" >') % (
widget.name, widget.value)
widget.name = widget.name + 'xx'
r += widget.render_content()
r += htmltext('</label>')
r += widget.title
r += htmltext('</li>')
r += htmltext('</ul>')
return r.getvalue()
METHODS = {
'condorcet-schulze': N_('Condorcet-Shulze'),
'irv': N_('Instant Run-off'),
'ranked_pairs': N_('Ranked pairs'),
'schulze-pr': N_('Schulze Proportional Ranking'),
'schulze-stv': N_('Schulze STV'),
'schulze-npr': N_('Schulze NPR'),
}
class VoteField(WidgetField):
key = 'vote-field'
description = N_('Ranked choice vote')
items = None
randomize_items = True
widget_class = VoteWidget
required_winners = None
winner_threshold = None
tallying_method = 'condorcet-schulze'
def perform_more_widget_changes(self, form, kwargs, edit = True):
kwargs['elements'] = self.items or []
kwargs['randomize_items'] = self.randomize_items
def fill_admin_form(self, form):
WidgetField.fill_admin_form(self, form)
form.add(WidgetList, 'items', title = _('Items'), element_type = StringWidget,
value = self.items, required = True,
element_kwargs = {'render_br': False, 'size': 50},
add_element_label = _('Add item'))
form.add(CheckboxWidget, 'randomize_items', title = _('Randomize Items'),
value = self.randomize_items)
form.add(SingleSelectWidget, 'tallying_method', title=_('Tallying method'),
value=self.tallying_method,
options=METHODS.items())
form.add(IntWidget, 'required_winners', title=_('Required winners'),
value=self.required_winners),
form.add(IntWidget, 'winner_threshold', title=_('Winner threshold'),
value=self.winner_threshold),
def get_admin_attributes(self):
return WidgetField.get_admin_attributes(self) + ['items',
'randomize_items', 'tallying_method', 'required_winners',
'winner_threshold']
def get_view_value(self, value):
r = TemplateIO(html=True)
r += htmltext('<ul>')
items = value.items()
items.sort(lambda x,y: cmp(x[1], y[1]))
for it in items:
if it[1]:
r += htmltext('<li>%s: %s</li>') % (it[1], it[0])
r += htmltext('</ul>')
return r.getvalue()
def stats(self, values):
'''
Compute vote result using the pyvotecore library.
'''
r = TemplateIO(html = True)
votes = [x.data.get(self.id) for x in values]
# build ballots
kwargs = { 'winner_threshold': self.winner_threshold,
'required_winners': self.required_winners }
if self.tallying_method == 'condorcet-schulze':
votes = [{ 'count': 1, 'ballot': x } for x in votes if x]
method = schulze_method.SchulzeMethod
elif self.tallying_method == 'irv':
votes = [ x.items() for x in votes ]
votes = [ sorted(x, cmp=lambda x,y: cmp(x[1],y[1])) for x in votes ]
votes = [ {'count':1, 'ballot': [ a for a,b in x ] } for x in votes ]
method = irv.IRV
elif self.tallying_method == 'ranked_pairs':
votes = [{ 'count': 1, 'ballot': x } for x in votes if x]
method = ranked_pairs.RankedPairs
elif self.tallying_method == 'schulze-pr':
votes = [{ 'count': 1, 'ballot': x } for x in votes if x]
method = schulze_pr.SchulzePR
elif self.tallying_method == 'schulze-stv':
votes = [{ 'count': 1, 'ballot': x } for x in votes if x]
method = schulze_stv.SchulzeSTV
elif self.tallying_method == 'schulze-npr':
votes = [{ 'count': 1, 'ballot': x } for x in votes if x]
method = schulze_npr.SchulzeNPR
else:
raise ValueError, 'unknown method', self.tallying_method
# Restrain arguments
accepted_args = method.__init__.im_func.func_code.co_varnames
for key in kwargs.keys():
if key not in accepted_args or kwargs.get(key) is None:
del kwargs[key]
# Run vote
result = method(votes, **kwargs).as_dict()
r += htmltext('<h4>%s</h4>') % _('Method')
r += htmltext('<p>%s</p>') % _(METHODS.get(self.tallying_method))
if 'candidates' in result:
r += htmltext('<h4>%s</h4>') % _('Candidates')
r += htmltext('<p>%s</p>') % (', '.join(result['candidates']))
if 'quota' in result:
r += htmltext('<h4>%s</h4>') % _('Quota')
r += htmltext('<p>%s</p>') % result.get('quota')
if 'rounds' in result:
for i, _round in enumerate(result['rounds']):
r += htmltext('<h4>%s</h4><div class="round">') % (_('Round %s') % (i+1))
if 'loser' in _round:
r += htmltext('<h5>%s</h5>') % _('Loser')
r += htmltext('<p>%s</p>') % _round['loser']
if 'tallies' in _round:
r += htmltext('<h5>%s</h5><ul>') % _('Tallies')
for a, b in _round['tallies'].iteritems():
r += htmltext('<li>%s: %s</li>') % (a,b)
r += htmltext('</ul>')
if 'tied_losers' in _round:
r += htmltext('<h5>%s</h5>') % _('Tied losers')
r += htmltext('<p>%s</p>') % ', '.join(list(_round['tied_losers']))
if 'winner' in _round:
r += htmltext('<h5>%s</h5>') % _('Winner')
r += htmltext('<p>%s</p>') % _round['winner']
r += htmltext('</div>')
if 'pairs' in result:
r += htmltext('<h4>%s</h4><dl>') % _('Pairs')
for a, b in result['pairs'].iteritems():
r += htmltext('<dt>%s</dt>') % ', '.join(a)
r += htmltext('<dd>%s</dd>') % b
r += htmltext('</dl>')
if 'strong_pairs' in result:
r += htmltext('<h4>%s</h4><dl>') % _('Strong pairs')
for a, b in result['strong_pairs'].iteritems():
r += htmltext('<dt>%s</dt>') % ', '.join(a)
r += htmltext('<dd>%s</dd>') % b
r += htmltext('</dl>')
if 'winner' in result:
r += htmltext('<h4>%s</h4>') % _('Winner')
r += htmltext('<p>%s</p>') % result['winner']
if 'winners' in result:
r += htmltext('<h4>%s</h4>') % _('Winners')
r += htmltext('<p>%s</p>') % ', '.join(result['winners'])
if 'order' in result:
r += htmltext('<h4>%s</h4>') % _('Order')
r += htmltext('<p>%s</p>') % ', '.join(result['order'])
# FIXME: show actions
# import pprint
# r += htmltext('<pre>%s</pre>') % pprint.pformat(result)
return r.getvalue()
register_field_class(VoteField)