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