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