Projet

Général

Profil

0001-general-remove-vote-extension-and-its-support-code-3.patch

Frédéric Péters, 11 novembre 2019 20:42

Télécharger (28,3 ko)

Voir les différences:

Subject: [PATCH] general: remove "vote" extension and its support code
 (#37573)

 extra/pyvotecore/vote_field.py | 277 --------------------------------
 extra/vote/anonymity.py        | 109 -------------
 extra/vote/ranked_items.py     | 281 ---------------------------------
 wcs/formdef.py                 |  12 +-
 wcs/forms/common.py            |   9 --
 5 files changed, 1 insertion(+), 687 deletions(-)
 delete mode 100644 extra/pyvotecore/vote_field.py
 delete mode 100644 extra/vote/anonymity.py
 delete mode 100644 extra/vote/ranked_items.py
extra/pyvotecore/vote_field.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2010  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17
# 02110-1301  USA
18

  
19
import random
20
from quixote.html import htmltext, TemplateIO
21
from wcs.qommon.form import CompositeWidget, IntWidget, WidgetList, StringWidget, \
22
        CheckboxWidget, SingleSelectWidget
23
from wcs.fields import WidgetField, register_field_class
24
from pyvotecore import schulze_method, irv, ranked_pairs, schulze_pr, \
25
        schulze_stv, schulze_npr
26

  
27
class VoteWidget(CompositeWidget):
28
    readonly = False
29

  
30
    def __init__(self, name, value = None, elements = None, **kwargs):
31
        CompositeWidget.__init__(self, name, value, **kwargs)
32
        self.element_names = {}
33

  
34
        if kwargs.has_key('title'):
35
            del kwargs['title']
36
        if kwargs.has_key('readonly'):
37
            if kwargs['readonly']:
38
                self.readonly = True
39
            del kwargs['readonly']
40
        if kwargs.has_key('required'):
41
            if kwargs['required']:
42
                self.required = True
43
            del kwargs['required']
44

  
45
        self.randomize_items = False
46
        if kwargs.has_key('randomize_items'):
47
            if kwargs['randomize_items']:
48
                self.randomize_items = True
49
            del kwargs['randomize_items']
50

  
51
        for v in elements:
52
            if type(v) is tuple:
53
                title = v[1]
54
                key = v[0]
55
                if type(key) is int:
56
                    name = 'element%d' % v[0]
57
                elif type(key) in (str, htmltext):
58
                    name = str('element%s' % v[0])
59
                    key = str(key)
60
                else:
61
                    raise NotImplementedError()
62
            else:
63
                title = v
64
                key = v
65
                name = 'element%d' % len(self.element_names.keys())
66

  
67
            if value:
68
                position = value.get(key)
69
            else:
70
                position = None
71
            self.add(IntWidget, name, title = title, value = position, size = 5, **kwargs)
72
            self.element_names[name] = key
73

  
74
        if self.randomize_items:
75
            random.shuffle(self.widgets)
76

  
77
        if self.readonly:
78
            def cmp_w(x, y):
79
                if x.value is None and y.value is None:
80
                    return 0
81
                if x.value is None:
82
                    return 1
83
                if y.value is None:
84
                    return -1
85
                return cmp(x.value, y.value)
86
            self.widgets.sort(cmp_w)
87

  
88

  
89
    def _parse(self, request):
90
        values = {}
91
        for name in self.element_names:
92
            value = self.get(name)
93
            values[self.element_names[name]] = value
94
            if type(value) is not int:
95
                self.get_widget(name).set_error(IntWidget.TYPE_ERROR)
96
        self.value = values or None
97

  
98
    def parse(self, request=None):
99
        value = CompositeWidget.parse(self, request=request)
100
        for widget in self.widgets:
101
            if widget.has_error():
102
                self.set_error(_('Some fields were not filled properly.'))
103
                break
104
        return value
105

  
106
    def render_content(self):
107
        r = TemplateIO(html=True)
108
        r += htmltext('<ul>')
109
        for widget in self.get_widgets():
110
            if widget.has_error():
111
                r += htmltext('<li class="error"><label>')
112
            else:
113
                r += htmltext('<li><label>')
114
            if self.readonly:
115
                widget.attrs['disabled'] = 'disabled'
116
                if widget.value:
117
                    r += htmltext('<input type="hidden" name="%s" value="%s" >') % (
118
                            widget.name, widget.value)
119
                widget.name = widget.name + 'xx'
120
            r += widget.render_content()
121
            r += htmltext('</label>')
122
            r += widget.title
123
            r += htmltext('</li>')
124
        r += htmltext('</ul>')
125
        return r.getvalue()
126

  
127
METHODS = {
128
        'condorcet-schulze': N_('Condorcet-Shulze'),
129
        'irv': N_('Instant Run-off'),
130
        'ranked_pairs': N_('Ranked pairs'),
131
        'schulze-pr': N_('Schulze Proportional Ranking'),
132
        'schulze-stv': N_('Schulze STV'),
133
        'schulze-npr': N_('Schulze NPR'),
134
        }
135

  
136
class VoteField(WidgetField):
137
    key = 'vote-field'
138
    description = N_('Ranked choice vote')
139

  
140
    items = None
141
    randomize_items = True
142
    widget_class = VoteWidget
143
    required_winners = None
144
    winner_threshold = None
145
    tallying_method = 'condorcet-schulze'
146

  
147
    def perform_more_widget_changes(self, form, kwargs, edit = True):
148
        kwargs['elements'] = self.items or []
149
        kwargs['randomize_items'] = self.randomize_items
150

  
151
    def fill_admin_form(self, form):
152
        WidgetField.fill_admin_form(self, form)
153
        form.add(WidgetList, 'items', title = _('Items'), element_type = StringWidget,
154
                value = self.items, required = True,
155
                element_kwargs = {'render_br': False, 'size': 50},
156
                add_element_label = _('Add item'))
157
        form.add(CheckboxWidget, 'randomize_items', title = _('Randomize Items'),
158
                value = self.randomize_items)
159
        form.add(SingleSelectWidget, 'tallying_method', title=_('Tallying method'),
160
                value=self.tallying_method,
161
                options=METHODS.items())
162
        form.add(IntWidget, 'required_winners', title=_('Required winners'),
163
                value=self.required_winners),
164
        form.add(IntWidget, 'winner_threshold', title=_('Winner threshold'),
165
                value=self.winner_threshold),
166

  
167
    def get_admin_attributes(self):
168
        return WidgetField.get_admin_attributes(self) + ['items',
169
                'randomize_items', 'tallying_method', 'required_winners',
170
                'winner_threshold']
171

  
172
    def get_view_value(self, value):
173
        r = TemplateIO(html=True)
174
        r += htmltext('<ul>')
175
        items = value.items()
176
        items.sort(lambda x,y: cmp(x[1], y[1]))
177
        for it in items:
178
            if it[1]:
179
                r += htmltext('<li>%s: %s</li>') % (it[1], it[0])
180
        r += htmltext('</ul>')
181
        return r.getvalue()
182

  
183
    def stats(self, values):
184
        '''
185
           Compute vote result using the pyvotecore library.
186
        '''
187
        r = TemplateIO(html = True)
188

  
189
        votes = [x.data.get(self.id) for x in values]
190
        # build ballots
191
        kwargs = { 'winner_threshold': self.winner_threshold,
192
                'required_winners': self.required_winners }
193
        if self.tallying_method == 'condorcet-schulze':
194
            votes = [{ 'count': 1, 'ballot': x } for x in votes if x]
195
            method = schulze_method.SchulzeMethod
196
        elif self.tallying_method == 'irv':
197
            votes = [ x.items() for x in votes ]
198
            votes = [ sorted(x, cmp=lambda x,y: cmp(x[1],y[1])) for x in votes ]
199
            votes = [ {'count':1, 'ballot': [ a for a,b in x ] } for x in votes ]
200

  
201
            method = irv.IRV
202
        elif self.tallying_method == 'ranked_pairs':
203
            votes = [{ 'count': 1, 'ballot': x } for x in votes if x]
204
            method = ranked_pairs.RankedPairs
205
        elif self.tallying_method == 'schulze-pr':
206
            votes = [{ 'count': 1, 'ballot': x } for x in votes if x]
207
            method = schulze_pr.SchulzePR
208
        elif self.tallying_method == 'schulze-stv':
209
            votes = [{ 'count': 1, 'ballot': x } for x in votes if x]
210
            method = schulze_stv.SchulzeSTV
211
        elif self.tallying_method == 'schulze-npr':
212
            votes = [{ 'count': 1, 'ballot': x } for x in votes if x]
213
            method = schulze_npr.SchulzeNPR
214
        else:
215
            raise ValueError, 'unknown method', self.tallying_method
216
        # Restrain arguments
217
        accepted_args = method.__init__.im_func.func_code.co_varnames
218
        for key in kwargs.keys():
219
            if key not in accepted_args or kwargs.get(key) is None:
220
                del kwargs[key]
221
        # Run vote
222
        result = method(votes, **kwargs).as_dict()
223

  
224
        r += htmltext('<h4>%s</h4>') % _('Method')
225
        r += htmltext('<p>%s</p>') % _(METHODS.get(self.tallying_method))
226
        if 'candidates' in result:
227
            r += htmltext('<h4>%s</h4>') % _('Candidates')
228
            r += htmltext('<p>%s</p>') % (', '.join(result['candidates']))
229
        if 'quota' in result:
230
            r += htmltext('<h4>%s</h4>') % _('Quota')
231
            r += htmltext('<p>%s</p>') % result.get('quota')
232
        if 'rounds' in result:
233
            for i, _round in enumerate(result['rounds']):
234
                r += htmltext('<h4>%s</h4><div class="round">') % (_('Round %s') % (i+1))
235
                if 'loser' in _round:
236
                    r += htmltext('<h5>%s</h5>') % _('Loser')
237
                    r += htmltext('<p>%s</p>') % _round['loser']
238
                if 'tallies' in _round:
239
                    r += htmltext('<h5>%s</h5><ul>') % _('Tallies')
240
                    for a, b in _round['tallies'].iteritems():
241
                        r += htmltext('<li>%s: %s</li>') % (a,b)
242
                    r += htmltext('</ul>')
243
                if 'tied_losers' in _round:
244
                    r += htmltext('<h5>%s</h5>') % _('Tied losers')
245
                    r += htmltext('<p>%s</p>') % ', '.join(list(_round['tied_losers']))
246
                if 'winner' in _round:
247
                    r += htmltext('<h5>%s</h5>') % _('Winner')
248
                    r += htmltext('<p>%s</p>') % _round['winner']
249
                r += htmltext('</div>')
250
        if 'pairs' in result:
251
            r += htmltext('<h4>%s</h4><dl>') % _('Pairs')
252
            for a, b in result['pairs'].iteritems():
253
                r += htmltext('<dt>%s</dt>') % ', '.join(a)
254
                r += htmltext('<dd>%s</dd>') % b
255
            r += htmltext('</dl>')
256
        if 'strong_pairs' in result:
257
            r += htmltext('<h4>%s</h4><dl>') % _('Strong pairs')
258
            for a, b in result['strong_pairs'].iteritems():
259
                r += htmltext('<dt>%s</dt>') % ', '.join(a)
260
                r += htmltext('<dd>%s</dd>') % b
261
            r += htmltext('</dl>')
262

  
263
        if 'winner' in result:
264
            r += htmltext('<h4>%s</h4>') % _('Winner')
265
            r += htmltext('<p>%s</p>') % result['winner']
266
        if 'winners' in result:
267
            r += htmltext('<h4>%s</h4>') % _('Winners')
268
            r += htmltext('<p>%s</p>') % ', '.join(result['winners'])
269
        if 'order' in result:
270
            r += htmltext('<h4>%s</h4>') % _('Order')
271
            r += htmltext('<p>%s</p>') % ', '.join(result['order'])
272
        # FIXME: show actions
273
        # import pprint
274
        # r += htmltext('<pre>%s</pre>') % pprint.pformat(result)
275
        return r.getvalue()
276

  
277
register_field_class(VoteField)
extra/vote/anonymity.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2010  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17
# 02110-1301  USA
18

  
19
from quixote import get_publisher
20

  
21
from wcs.fields import WidgetField, register_field_class
22
from wcs.qommon.form import *
23

  
24
class VoteAnonymityWidget(CheckboxWidget):
25
    vote_anonymity = 'anonymous'
26

  
27
    def __init__(self, name, value = None, elements = None, **kwargs):
28
        if kwargs and kwargs.get('vote_anonymity'):
29
            self.vote_anonymity = kwargs.get('vote_anonymity')
30
        if kwargs and 'required' in kwargs:
31
            del kwargs['required']
32
        CheckboxWidget.__init__(self, name, value=value, elements=elements, **kwargs)
33

  
34
    def render(self):
35
        if self.vote_anonymity != 'choice':
36
            # XXX: include info for user ?
37
            return ''
38
        return CheckboxWidget.render(self)
39

  
40
    def render_title(self, title):
41
        return CheckboxWidget.render_title(self, _('Anonymous Voting'))
42

  
43
    def render_content(self):
44
        value = True
45
        if self.value:
46
            if self.value[0] == 'anonymous':
47
                value = True
48
            else:
49
                value = False
50
        return htmltag('input', xml_end=True,
51
                       type='checkbox',
52
                       name=self.name,
53
                       value='yes',
54
                       checked=value and 'checked' or None,
55
                       **self.attrs)
56

  
57
    def _parse(self, request):
58
        if self.vote_anonymity == 'anonymous':
59
            self.value = ('anonymous', None)
60
        elif self.vote_anonymity == 'public':
61
            self.value = ('public', request.user.id)
62
        else:
63
            if type(request.form.get(self.name)) is tuple:
64
                self.value = request.form.get(self.name)
65
            else:
66
                CheckboxWidget._parse(self, request)
67
                if self.value is True:
68
                    self.value = ('anonymous', None)
69
                else:
70
                    self.value = ('public', request.user.id)
71

  
72

  
73
class VoteAnonymityField(WidgetField):
74
    key = 'vote-anonymity'
75
    description = N_('Vote Anonymity')
76

  
77
    widget_class = VoteAnonymityWidget
78
    vote_anonymity = 'anonymous'
79
        # possible values:
80
        #  'anonymous': anonymous vote
81
        #  'public': public vote
82
        #  'choice': anonymous or public, choice of voter
83

  
84
    def fill_admin_form(self, form):
85
        form.add(StringWidget, 'label', title = _('Label'), value = self.label,
86
                required = True, size = 50)
87
        form.add(SingleSelectWidget, 'vote_anonymity', title = _('Vote Anonymity'),
88
                value = self.vote_anonymity,
89
                options = [('anonymous', _('Anonymous Vote')),
90
                    ('public', _('Public Vote')),
91
                    ('choice', _('Choice of Voter'))])
92

  
93
    def get_admin_attributes(self):
94
        return ['label', 'vote_anonymity']
95

  
96
    def perform_more_widget_changes(self, form, kwargs, edit = True):
97
        kwargs['vote_anonymity'] = self.vote_anonymity
98

  
99
    def get_view_value(self, value):
100
        public = False
101
        if self.vote_anonymity == 'choice':
102
            if value and value[0] == 'public':
103
                public = True
104
        if public:
105
            return get_publisher().user_class.get(value[1]).get_display_name()
106
        else:
107
            return _('Anonymous') # XXX: include token?
108

  
109
register_field_class(VoteAnonymityField)
extra/vote/ranked_items.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2010  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17
# 02110-1301  USA
18

  
19
import random
20
from quixote.html import htmltext, TemplateIO
21
from wcs.qommon.form import *
22
from wcs.fields import WidgetField, register_field_class
23

  
24
class RankedItemsWidget(CompositeWidget):
25
    readonly = False
26

  
27
    def __init__(self, name, value = None, elements = None, **kwargs):
28
        CompositeWidget.__init__(self, name, value, **kwargs)
29
        self.element_names = {}
30

  
31
        if kwargs.has_key('title'):
32
            del kwargs['title']
33
        if kwargs.has_key('readonly'):
34
            if kwargs['readonly']:
35
                self.readonly = True
36
            del kwargs['readonly']
37
        if kwargs.has_key('required'):
38
            if kwargs['required']:
39
                self.required = True
40
            del kwargs['required']
41

  
42
        self.randomize_items = False
43
        if kwargs.has_key('randomize_items'):
44
            if kwargs['randomize_items']:
45
                self.randomize_items = True
46
            del kwargs['randomize_items']
47

  
48
        for v in elements:
49
            if type(v) is tuple:
50
                title = v[1]
51
                key = v[0]
52
                if type(key) is int:
53
                    name = 'element%d' % v[0]
54
                elif type(key) in (str, htmltext):
55
                    name = str('element%s' % v[0])
56
                    key = str(key)
57
                else:
58
                    raise NotImplementedError()
59
            else:
60
                title = v
61
                key = v
62
                name = 'element%d' % len(self.element_names.keys())
63

  
64
            if value:
65
                position = value.get(key)
66
            else:
67
                position = None
68
            self.add(IntWidget, name, title = title, value = position, size = 5, **kwargs)
69
            self.element_names[name] = key
70

  
71
        if self.randomize_items:
72
            random.shuffle(self.widgets)
73

  
74
        if self.readonly:
75
            def cmp_w(x, y):
76
                if x.value is None and y.value is None:
77
                    return 0
78
                if x.value is None:
79
                    return 1
80
                if y.value is None:
81
                    return -1
82
                return cmp(x.value, y.value)
83
            self.widgets.sort(cmp_w)
84

  
85

  
86
    def _parse(self, request):
87
        values = {}
88
        for name in self.element_names:
89
            value = self.get(name)
90
            values[self.element_names[name]] = value
91
            if type(value) is not int:
92
                self.get_widget(name).set_error(IntWidget.TYPE_ERROR)
93
        self.value = values or None
94

  
95
    def parse(self, request=None):
96
        value = CompositeWidget.parse(self, request=request)
97
        for widget in self.widgets:
98
            if widget.has_error():
99
                self.set_error(_('Some fields were not filled properly.'))
100
                break
101
        return value
102

  
103
    def render_content(self):
104
        r = TemplateIO(html=True)
105
        r += htmltext('<ul>')
106
        for widget in self.get_widgets():
107
            if widget.has_error():
108
                r += htmltext('<li class="error"><label>')
109
            else:
110
                r += htmltext('<li><label>')
111
            if self.readonly:
112
                widget.attrs['disabled'] = 'disabled'
113
                if widget.value:
114
                    r += htmltext('<input type="hidden" name="%s" value="%s" >') % (
115
                            widget.name, widget.value)
116
                widget.name = widget.name + 'xx'
117
            r += widget.render_content()
118
            r += htmltext('</label>')
119
            r += widget.title
120
            r += htmltext('</li>')
121
        r += htmltext('</ul>')
122
        return r.getvalue()
123

  
124

  
125
class RankedItemsField(WidgetField):
126
    key = 'ranked-items'
127
    description = N_('Ranked Items')
128

  
129
    items = None
130
    randomize_items = True
131
    widget_class = RankedItemsWidget
132

  
133
    def perform_more_widget_changes(self, form, kwargs, edit = True):
134
        kwargs['elements'] = self.items or []
135
        kwargs['randomize_items'] = self.randomize_items
136

  
137
    def fill_admin_form(self, form):
138
        WidgetField.fill_admin_form(self, form)
139
        form.add(WidgetList, 'items', title = _('Items'), element_type = StringWidget,
140
                value = self.items, required = True,
141
                element_kwargs = {'render_br': False, 'size': 50},
142
                add_element_label = _('Add item'))
143
        form.add(CheckboxWidget, 'randomize_items', title = _('Randomize Items'),
144
                value = self.randomize_items)
145

  
146
    def get_admin_attributes(self):
147
        return WidgetField.get_admin_attributes(self) + ['items', 'randomize_items']
148

  
149
    def get_view_value(self, value):
150
        r = TemplateIO(html=True)
151
        r += htmltext('<ul>')
152
        items = value.items()
153
        items.sort(lambda x,y: cmp(x[1], y[1]))
154
        for it in items:
155
            if it[1]:
156
                r += htmltext('<li>%s: %s</li>') % (it[1], it[0])
157
        r += htmltext('</ul>')
158
        return r.getvalue()
159

  
160
    def stats(self, values):
161
        r = TemplateIO(html = True)
162

  
163
        # hardcoded to condorcet for the moment
164
        candidates = self.items
165
        # compute matrix
166
        matrix = {}
167
        for c1 in candidates:
168
            matrix[c1] = {}
169
            for c2 in candidates:
170
                matrix[c1][c2] = 0
171
        votes = [x.data.get(self.id) for x in values]
172
        votes = [x for x in votes if x]
173
        for vote in votes:
174
            for c1 in candidates:
175
                for c2 in candidates:
176
                    if c1 == c2:
177
                        continue
178
                    vote_a = vote.get(c1)
179
                    vote_b = vote.get(c2)
180
                    if vote_a is None:
181
                        vote_a = 99999 # XXX MAX_INT
182
                    if vote_b is None:
183
                        vote_b = 99999 # idem
184
                    if int(vote_a) == int(vote_b):
185
                        matrix[c1][c2] += 0.5
186
                    elif int(vote_a) < int(vote_b):
187
                        matrix[c1][c2] += 1
188
        import pprint
189
        pprint.pprint(matrix)
190
        # compute ratings
191
        ratings = {}
192
        for c1 in candidates:
193
            ratings[c1] = {'win': [], 'loss': [], 'tie': [], 'worst': 0}
194
            for c2 in candidates:
195
                if c1 == c2:
196
                    continue
197
                delta = matrix[c1][c2] - matrix[c2][c1]
198
                if delta > 0:
199
                    ratings[c1]['win'].append(c2)
200
                elif delta < 0:
201
                    ratings[c1]['loss'].append(c2)
202
                    if delta < ratings[c1]['worst']:
203
                        ratings[c1]['worst'] = -delta
204
                else:
205
                    ratings[c1]['tie'].append(c2)
206

  
207
        pprint.pprint(ratings)
208

  
209
        # compute winner
210
        winners = []
211
        remaining = candidates[:]
212
        for c1 in remaining:
213
            rating = ratings[c1]
214
            winner = True
215
            for loss in rating['loss']:
216
                if loss not in winners:
217
                    winner = False
218
                    break
219
            if not winner:
220
                continue
221
            for tie in rating['tie']:
222
                if tie not in winners:
223
                    winner = False
224
                    break
225
            if not winner:
226
                continue
227
            winners.append(c1)
228
            remaining.remove(c1)
229
            break
230
        else:
231
            narrowest = None
232
            winners = []
233
            for c2 in remaining:
234
                rating = ratings[c2]
235
                if narrowest is None or rating['worst'] < narrowest:
236
                    narrowest = rating['worst']
237
                    winners = [c2]
238
                elif rating['worst'] == narrowest:
239
                    winners.append(c2)
240

  
241
        candidates.sort(lambda x,y: cmp(len(ratings[x]['win']), len(ratings[y]['win'])))
242

  
243
        r += htmltext('<table>'
244
                '<thead>'
245
                '<tr>'
246
                '<td></td>')
247
        for c in candidates:
248
            r += htmltext('<th>%s</th>' % c)
249
        r += htmltext('</tr>'
250
                '</thead>'
251
                '<tbody>')
252
        for c1 in candidates:
253
            r += htmltext('<tr>'
254
                    '<th>%s</th>' % c1)
255
            for c2 in candidates:
256
                if c2 == c1:
257
                    r += htmltext('<td></td>')
258
                else:
259
                    if matrix[c1][c2] > matrix[c2][c1]:
260
                        color = '#0f0'
261
                    elif matrix[c1][c2] == matrix[c2][c1]:
262
                        color = '#ff0'
263
                    else:
264
                        color = '#f00'
265
                    r += htmltext('<td style="background: %s">' % color)
266
                    r += '%.1f' % matrix[c1][c2]
267
                    r += htmltext('</td>')
268
            r += htmltext('</tr>')
269
        r += htmltext('</tbody>'
270
                '</table>'
271
                '<p>')
272
        r += _('Winner:')
273
        r += ' '
274
        r += ' '.join(winners)
275
        r += htmltext('</p>')
276
        return r.getvalue()
277

  
278
register_field_class(RankedItemsField)
279

  
280

  
281

  
wcs/formdef.py
1186 1186
    def get_detailed_email_form(self, formdata, url):
1187 1187
        details = []
1188 1188

  
1189
        display_username = True
1190
        # this is custom code so it is possible to mark forms as anonyms, this
1191
        # is done through the VoteAnonymity field, this is very specific but
1192
        # isn't generalised yet into an useful extension mechanism, as it's not
1193
        # clear at the moment what could be useful.
1194
        for f in self.fields:
1195
            if f.key == 'vote-anonymity':
1196
                display_username = False
1197
                break
1198

  
1199
        if display_username and formdata.user_id and formdata.user:
1189
        if formdata.user_id and formdata.user:
1200 1190
            details.append(_('User name:'))
1201 1191
            details.append('  %s' % formdata.user.name)
1202 1192
            details.append('')
wcs/forms/common.py
357 357
            except KeyError:
358 358
                user = None
359 359

  
360
        # this is custom code so it is possible to mark forms as anonyms, this
361
        # is done through the VoteAnonymity field, this is very specific but
362
        # isn't generalised yet into an useful extension mechanism, as it's not
363
        # clear at the moment what could be useful.
364
        for f in self.formdef.fields:
365
            if f.key == 'vote-anonymity':
366
                user = None
367
                break
368

  
369 360
        r = TemplateIO(html=True)
370 361
        klasses = 'foldable'
371 362
        if self.should_fold_summary(mine, request_user):
372
-