Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
27.88% covered (danger)
27.88%
29 / 104
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
PreferentialBallot
27.88% covered (danger)
27.88%
29 / 104
14.29% covered (danger)
14.29%
1 / 7
203.52
0.00% covered (danger)
0.00%
0 / 1
 getTallyTypes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCreateDescriptors
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 getQuestionForm
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
6
 submitQuestion
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
8
 packRecord
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 unpackRecord
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 convertScores
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3namespace MediaWiki\Extension\SecurePoll\Ballots;
4
5use MediaWiki\Extension\SecurePoll\Entities\Question;
6use OOUI\FieldsetLayout;
7use OOUI\HorizontalLayout;
8use OOUI\HtmlSnippet;
9use OOUI\LabelWidget;
10use OOUI\NumberInputWidget;
11
12/**
13 * Ballot for preferential voting
14 * Properties:
15 *     shuffle-questions
16 *     shuffle-options
17 *     must-rank-all
18 */
19class PreferentialBallot extends Ballot {
20    public static function getTallyTypes() {
21        return [ 'schulze' ];
22    }
23
24    public static function getCreateDescriptors() {
25        $ret = parent::getCreateDescriptors();
26        $ret['election'] += [
27            'must-rank-all' => [
28                'label-message' => 'securepoll-create-label-must_rank_all',
29                'type' => 'check',
30                'hidelabel' => true,
31                'SecurePoll_type' => 'property',
32            ],
33        ];
34
35        return $ret;
36    }
37
38    /**
39     * @param Question $question
40     * @param array $options
41     * @return FieldsetLayout
42     */
43    public function getQuestionForm( $question, $options ) {
44        $name = 'securepoll_q' . $question->getId();
45        $fieldset = new FieldsetLayout();
46        foreach ( $options as $option ) {
47            $optionHTML = $option->parseMessageInline( 'text' );
48            $optionId = $option->getId();
49            $inputId = "{$name}_opt{$optionId}";
50            $oldValue = $this->getRequest()->getVal( $inputId, '' );
51
52            $widget = new NumberInputWidget( [
53                'name' => $inputId,
54                'default' => $oldValue,
55                'min' => 1,
56                'max' => 999,
57                'required' => $this->election->getProperty( 'must-rank-all' ),
58            ] );
59
60            $label = new LabelWidget( [
61                'label' => new HtmlSnippet( $this->errorLocationIndicator( $inputId ) . $optionHTML ),
62                'input' => $widget,
63            ] );
64
65            $fieldset->appendContent( new HorizontalLayout(
66                [
67                    'classes' => [ 'securepoll-option-preferential' ],
68                    'items' => [
69                        $widget,
70                        $label,
71                    ],
72                ]
73            ) );
74        }
75
76        return $fieldset;
77    }
78
79    /**
80     * @param Question $question
81     * @param BallotStatus $status
82     * @return string
83     */
84    public function submitQuestion( $question, $status ) {
85        $options = $question->getOptions();
86        $record = '';
87        $ok = true;
88        foreach ( $options as $option ) {
89            $id = 'securepoll_q' . $question->getId() . '_opt' . $option->getId();
90            $rank = $this->getRequest()->getVal( $id );
91
92            if ( is_numeric( $rank ) ) {
93                if ( $rank <= 0 || $rank >= 1000 ) {
94                    $status->spFatal( 'securepoll-invalid-rank', $id, false );
95                    $ok = false;
96                    continue;
97                } else {
98                    $rank = intval( $rank );
99                }
100            } elseif ( strval( $rank ) === '' ) {
101                if ( $this->election->getProperty( 'must-rank-all' ) ) {
102                    $status->spFatal( 'securepoll-unranked-options', $id, false );
103                    $ok = false;
104                    continue;
105                } else {
106                    $rank = 1000;
107                }
108            } else {
109                $status->spFatal( 'securepoll-invalid-rank', $id, false );
110                $ok = false;
111                continue;
112            }
113            $record .= $this->packRecord( $question, $option, $rank );
114        }
115        if ( $ok ) {
116            return $record;
117        }
118    }
119
120    public function packRecord( $question, $option, $rank ) {
121        return sprintf(
122            'Q%08X-A%08X-R%08X--',
123            $question->getId(),
124            $option->getId(),
125            $rank
126        );
127    }
128
129    public function unpackRecord( $record ) {
130        $ranks = [];
131        $itemLength = 3 * 8 + 7;
132        for ( $offset = 0, $len = strlen( $record ); $offset < $len; $offset += $itemLength ) {
133            if ( !preg_match(
134                '/Q([0-9A-F]{8})-A([0-9A-F]{8})-R([0-9A-F]{8})--/A',
135                $record,
136                $m,
137                0,
138                $offset
139            )
140            ) {
141                wfDebug( __METHOD__ . ": regex doesn't match\n" );
142
143                return false;
144            }
145            $qid = intval( base_convert( $m[1], 16, 10 ) );
146            $oid = intval( base_convert( $m[2], 16, 10 ) );
147            $rank = intval( base_convert( $m[3], 16, 10 ) );
148            $ranks[$qid][$oid] = $rank;
149        }
150
151        return $ranks;
152    }
153
154    public function convertScores( $scores, $params = [] ) {
155        $result = [];
156        foreach ( $this->election->getQuestions() as $question ) {
157            $qid = $question->getId();
158            if ( !isset( $scores[$qid] ) ) {
159                return false;
160            }
161            $s = '';
162            $qscores = $scores[$qid];
163            ksort( $qscores );
164            $first = true;
165            foreach ( $qscores as $rank ) {
166                if ( $first ) {
167                    $first = false;
168                } else {
169                    $s .= ', ';
170                }
171                if ( $rank == 1000 ) {
172                    $s .= '-';
173                } else {
174                    $s .= $rank;
175                }
176            }
177            $result[$qid] = $s;
178        }
179
180        return $result;
181    }
182}