general: remove "vote" extension and its support code (#37573)

This commit is contained in:
Frédéric Péters 2019-11-11 20:41:50 +01:00
parent f9590d9d4b
commit 1b7a956ad6
5 changed files with 1 additions and 687 deletions

View File

@ -1,277 +0,0 @@
# 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 wcs.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)

View File

@ -1,109 +0,0 @@
# 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
from quixote import get_publisher
from wcs.fields import WidgetField, register_field_class
from wcs.qommon.form import *
class VoteAnonymityWidget(CheckboxWidget):
vote_anonymity = 'anonymous'
def __init__(self, name, value = None, elements = None, **kwargs):
if kwargs and kwargs.get('vote_anonymity'):
self.vote_anonymity = kwargs.get('vote_anonymity')
if kwargs and 'required' in kwargs:
del kwargs['required']
CheckboxWidget.__init__(self, name, value=value, elements=elements, **kwargs)
def render(self):
if self.vote_anonymity != 'choice':
# XXX: include info for user ?
return ''
return CheckboxWidget.render(self)
def render_title(self, title):
return CheckboxWidget.render_title(self, _('Anonymous Voting'))
def render_content(self):
value = True
if self.value:
if self.value[0] == 'anonymous':
value = True
else:
value = False
return htmltag('input', xml_end=True,
type='checkbox',
name=self.name,
value='yes',
checked=value and 'checked' or None,
**self.attrs)
def _parse(self, request):
if self.vote_anonymity == 'anonymous':
self.value = ('anonymous', None)
elif self.vote_anonymity == 'public':
self.value = ('public', request.user.id)
else:
if type(request.form.get(self.name)) is tuple:
self.value = request.form.get(self.name)
else:
CheckboxWidget._parse(self, request)
if self.value is True:
self.value = ('anonymous', None)
else:
self.value = ('public', request.user.id)
class VoteAnonymityField(WidgetField):
key = 'vote-anonymity'
description = N_('Vote Anonymity')
widget_class = VoteAnonymityWidget
vote_anonymity = 'anonymous'
# possible values:
# 'anonymous': anonymous vote
# 'public': public vote
# 'choice': anonymous or public, choice of voter
def fill_admin_form(self, form):
form.add(StringWidget, 'label', title = _('Label'), value = self.label,
required = True, size = 50)
form.add(SingleSelectWidget, 'vote_anonymity', title = _('Vote Anonymity'),
value = self.vote_anonymity,
options = [('anonymous', _('Anonymous Vote')),
('public', _('Public Vote')),
('choice', _('Choice of Voter'))])
def get_admin_attributes(self):
return ['label', 'vote_anonymity']
def perform_more_widget_changes(self, form, kwargs, edit = True):
kwargs['vote_anonymity'] = self.vote_anonymity
def get_view_value(self, value):
public = False
if self.vote_anonymity == 'choice':
if value and value[0] == 'public':
public = True
if public:
return get_publisher().user_class.get(value[1]).get_display_name()
else:
return _('Anonymous') # XXX: include token?
register_field_class(VoteAnonymityField)

View File

@ -1,281 +0,0 @@
# 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 wcs.qommon.form import *
from wcs.fields import WidgetField, register_field_class
class RankedItemsWidget(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()
class RankedItemsField(WidgetField):
key = 'ranked-items'
description = N_('Ranked Items')
items = None
randomize_items = True
widget_class = RankedItemsWidget
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)
def get_admin_attributes(self):
return WidgetField.get_admin_attributes(self) + ['items', 'randomize_items']
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):
r = TemplateIO(html = True)
# hardcoded to condorcet for the moment
candidates = self.items
# compute matrix
matrix = {}
for c1 in candidates:
matrix[c1] = {}
for c2 in candidates:
matrix[c1][c2] = 0
votes = [x.data.get(self.id) for x in values]
votes = [x for x in votes if x]
for vote in votes:
for c1 in candidates:
for c2 in candidates:
if c1 == c2:
continue
vote_a = vote.get(c1)
vote_b = vote.get(c2)
if vote_a is None:
vote_a = 99999 # XXX MAX_INT
if vote_b is None:
vote_b = 99999 # idem
if int(vote_a) == int(vote_b):
matrix[c1][c2] += 0.5
elif int(vote_a) < int(vote_b):
matrix[c1][c2] += 1
import pprint
pprint.pprint(matrix)
# compute ratings
ratings = {}
for c1 in candidates:
ratings[c1] = {'win': [], 'loss': [], 'tie': [], 'worst': 0}
for c2 in candidates:
if c1 == c2:
continue
delta = matrix[c1][c2] - matrix[c2][c1]
if delta > 0:
ratings[c1]['win'].append(c2)
elif delta < 0:
ratings[c1]['loss'].append(c2)
if delta < ratings[c1]['worst']:
ratings[c1]['worst'] = -delta
else:
ratings[c1]['tie'].append(c2)
pprint.pprint(ratings)
# compute winner
winners = []
remaining = candidates[:]
for c1 in remaining:
rating = ratings[c1]
winner = True
for loss in rating['loss']:
if loss not in winners:
winner = False
break
if not winner:
continue
for tie in rating['tie']:
if tie not in winners:
winner = False
break
if not winner:
continue
winners.append(c1)
remaining.remove(c1)
break
else:
narrowest = None
winners = []
for c2 in remaining:
rating = ratings[c2]
if narrowest is None or rating['worst'] < narrowest:
narrowest = rating['worst']
winners = [c2]
elif rating['worst'] == narrowest:
winners.append(c2)
candidates.sort(lambda x,y: cmp(len(ratings[x]['win']), len(ratings[y]['win'])))
r += htmltext('<table>'
'<thead>'
'<tr>'
'<td></td>')
for c in candidates:
r += htmltext('<th>%s</th>' % c)
r += htmltext('</tr>'
'</thead>'
'<tbody>')
for c1 in candidates:
r += htmltext('<tr>'
'<th>%s</th>' % c1)
for c2 in candidates:
if c2 == c1:
r += htmltext('<td></td>')
else:
if matrix[c1][c2] > matrix[c2][c1]:
color = '#0f0'
elif matrix[c1][c2] == matrix[c2][c1]:
color = '#ff0'
else:
color = '#f00'
r += htmltext('<td style="background: %s">' % color)
r += '%.1f' % matrix[c1][c2]
r += htmltext('</td>')
r += htmltext('</tr>')
r += htmltext('</tbody>'
'</table>'
'<p>')
r += _('Winner:')
r += ' '
r += ' '.join(winners)
r += htmltext('</p>')
return r.getvalue()
register_field_class(RankedItemsField)

View File

@ -1184,17 +1184,7 @@ class FormDef(StorableObject):
def get_detailed_email_form(self, formdata, url):
details = []
display_username = True
# this is custom code so it is possible to mark forms as anonyms, this
# is done through the VoteAnonymity field, this is very specific but
# isn't generalised yet into an useful extension mechanism, as it's not
# clear at the moment what could be useful.
for f in self.fields:
if f.key == 'vote-anonymity':
display_username = False
break
if display_username and formdata.user_id and formdata.user:
if formdata.user_id and formdata.user:
details.append(_('User name:'))
details.append(' %s' % formdata.user.name)
details.append('')

View File

@ -350,15 +350,6 @@ class FormStatusPage(Directory, FormTemplateMixin):
except KeyError:
user = None
# this is custom code so it is possible to mark forms as anonyms, this
# is done through the VoteAnonymity field, this is very specific but
# isn't generalised yet into an useful extension mechanism, as it's not
# clear at the moment what could be useful.
for f in self.formdef.fields:
if f.key == 'vote-anonymity':
user = None
break
r = TemplateIO(html=True)
klasses = 'foldable'
if self.should_fold_summary(mine, request_user):