diff --git a/extra/pyvotecore/vote_field.py b/extra/pyvotecore/vote_field.py new file mode 100644 index 000000000..d4abd0fe8 --- /dev/null +++ b/extra/pyvotecore/vote_field.py @@ -0,0 +1,277 @@ +# 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('') + 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('') + 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('

%s

') % _('Method') + r += htmltext('

%s

') % _(METHODS.get(self.tallying_method)) + if 'candidates' in result: + r += htmltext('

%s

') % _('Candidates') + r += htmltext('

%s

') % (', '.join(result['candidates'])) + if 'quota' in result: + r += htmltext('

%s

') % _('Quota') + r += htmltext('

%s

') % result.get('quota') + if 'rounds' in result: + for i, _round in enumerate(result['rounds']): + r += htmltext('

%s

') % (_('Round %s') % (i+1)) + if 'loser' in _round: + r += htmltext('
%s
') % _('Loser') + r += htmltext('

%s

') % _round['loser'] + if 'tallies' in _round: + r += htmltext('
%s
') + if 'tied_losers' in _round: + r += htmltext('
%s
') % _('Tied losers') + r += htmltext('

%s

') % ', '.join(list(_round['tied_losers'])) + if 'winner' in _round: + r += htmltext('
%s
') % _('Winner') + r += htmltext('

%s

') % _round['winner'] + r += htmltext('
') + if 'pairs' in result: + r += htmltext('

%s

') % _('Pairs') + for a, b in result['pairs'].iteritems(): + r += htmltext('
%s
') % ', '.join(a) + r += htmltext('
%s
') % b + r += htmltext('
') + if 'strong_pairs' in result: + r += htmltext('

%s

') % _('Strong pairs') + for a, b in result['strong_pairs'].iteritems(): + r += htmltext('
%s
') % ', '.join(a) + r += htmltext('
%s
') % b + r += htmltext('
') + + if 'winner' in result: + r += htmltext('

%s

') % _('Winner') + r += htmltext('

%s

') % result['winner'] + if 'winners' in result: + r += htmltext('

%s

') % _('Winners') + r += htmltext('

%s

') % ', '.join(result['winners']) + if 'order' in result: + r += htmltext('

%s

') % _('Order') + r += htmltext('

%s

') % ', '.join(result['order']) + # FIXME: show actions + # import pprint + # r += htmltext('
%s
') % pprint.pformat(result) + return r.getvalue() + +register_field_class(VoteField)