Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
35.16% |
32 / 91 |
|
18.75% |
3 / 16 |
CRAP | |
0.00% |
0 / 1 |
Ballot | |
35.16% |
32 / 91 |
|
18.75% |
3 / 16 |
225.68 | |
0.00% |
0 / 1 |
getTallyTypes | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCreateDescriptors | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
2 | |||
getQuestionForm | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getMessageNames | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRequest | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getMessageLocalizer | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
msg | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUserLang | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
initRequest | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
submitForm | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
submitQuestion | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
unpackRecord | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
convertRecord | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
convertScores | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
factory | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getForm | |
92.31% |
24 / 26 |
|
0.00% |
0 / 1 |
5.01 | |||
setErrorStatus | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
errorLocationIndicator | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
formatStatus | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\SecurePoll\Ballots; |
4 | |
5 | use InvalidArgumentException; |
6 | use Language; |
7 | use LogicException; |
8 | use MediaWiki\Extension\SecurePoll\Context; |
9 | use MediaWiki\Extension\SecurePoll\Entities\Election; |
10 | use MediaWiki\Extension\SecurePoll\Entities\Entity; |
11 | use MediaWiki\Extension\SecurePoll\Entities\Question; |
12 | use MediaWiki\Request\WebRequest; |
13 | use MediaWiki\Status\Status; |
14 | use MessageLocalizer; |
15 | |
16 | /** |
17 | * Parent class for ballot forms. This is the UI component of a voting method. |
18 | */ |
19 | abstract class Ballot { |
20 | /** @var Election */ |
21 | public $election; |
22 | /** @var Context */ |
23 | public $context; |
24 | /** @var string|null */ |
25 | public $currentVote; |
26 | /** @var true[]|null */ |
27 | public $prevErrorIds; |
28 | /** @var true[]|null */ |
29 | public $usedErrorIds; |
30 | /** @var BallotStatus|null */ |
31 | public $prevStatus; |
32 | /** @var WebRequest|null */ |
33 | private $request; |
34 | /** @var MessageLocalizer|null */ |
35 | private $messageLocalizer; |
36 | /** @var Language|null */ |
37 | private $userLang; |
38 | |
39 | /** @var string[] */ |
40 | public const BALLOT_TYPES = [ |
41 | 'approval' => ApprovalBallot::class, |
42 | 'preferential' => PreferentialBallot::class, |
43 | 'choose' => ChooseBallot::class, |
44 | 'radio-range' => RadioRangeBallot::class, |
45 | 'radio-range-comment' => RadioRangeCommentBallot::class, |
46 | 'stv' => STVBallot::class, |
47 | ]; |
48 | |
49 | /** |
50 | * Get a list of names of tallying methods, which may be used to produce a |
51 | * result from this ballot type. |
52 | * @return array |
53 | */ |
54 | public static function getTallyTypes() { |
55 | throw new LogicException( "Subclass must override ::getTallyTypes()" ); |
56 | } |
57 | |
58 | /** |
59 | * Return descriptors for any properties this type requires for poll |
60 | * creation, for the election, questions, and options. |
61 | * |
62 | * The returned array should have three keys, "election", "question", and |
63 | * "option", each mapping to an array of HTMLForm descriptors. |
64 | * |
65 | * The descriptors should have an additional key, "SecurePoll_type", with |
66 | * the value being "property" or "message". |
67 | * |
68 | * @return array |
69 | */ |
70 | public static function getCreateDescriptors() { |
71 | return [ |
72 | 'election' => [ |
73 | 'shuffle-questions' => [ |
74 | 'label-message' => 'securepoll-create-label-shuffle_questions', |
75 | 'type' => 'check', |
76 | 'hidelabel' => true, |
77 | 'SecurePoll_type' => 'property', |
78 | ], |
79 | 'shuffle-options' => [ |
80 | 'label-message' => 'securepoll-create-label-shuffle_options', |
81 | 'type' => 'check', |
82 | 'hidelabel' => true, |
83 | 'SecurePoll_type' => 'property', |
84 | ], |
85 | ], |
86 | 'question' => [], |
87 | 'option' => [], |
88 | ]; |
89 | } |
90 | |
91 | /** |
92 | * Get the HTML form segment for a single question |
93 | * @param Question $question |
94 | * @param array $options Array of options, in the order they should be displayed |
95 | * @return \OOUI\FieldsetLayout |
96 | */ |
97 | abstract public function getQuestionForm( $question, $options ); |
98 | |
99 | /** |
100 | * Get any extra messages that this ballot type uses to render questions. |
101 | * Used to get the list of translatable messages for TranslatePage. |
102 | * @param Entity|null $entity |
103 | * @return array |
104 | * @see Election::getMessageNames() |
105 | */ |
106 | public function getMessageNames( Entity $entity = null ) { |
107 | return []; |
108 | } |
109 | |
110 | /** |
111 | * Get the request if it has been set, otherwise throw an exception. |
112 | * |
113 | * @return WebRequest |
114 | */ |
115 | protected function getRequest(): WebRequest { |
116 | if ( !$this->request ) { |
117 | throw new LogicException( |
118 | 'Ballot::initRequest() must be called before Ballot::getRequest()' ); |
119 | } |
120 | return $this->request; |
121 | } |
122 | |
123 | /** |
124 | * Get the MessageLocalizer if it has been set, otherwise throw an exception |
125 | * |
126 | * @return MessageLocalizer |
127 | */ |
128 | private function getMessageLocalizer(): MessageLocalizer { |
129 | if ( !$this->messageLocalizer ) { |
130 | throw new LogicException( |
131 | 'Ballot::initRequest() must be called before Ballot::getMessageLocalizer()' ); |
132 | } |
133 | return $this->messageLocalizer; |
134 | } |
135 | |
136 | /** |
137 | * Get a MediaWiki message. setMessageLocalizer() must have been called. |
138 | * |
139 | * This can be used instead of SecurePoll's native message system if the |
140 | * message does not vary depending on the election, and if there are no |
141 | * security concerns with allowing people who are not admins of the election |
142 | * to set the text. |
143 | * |
144 | * @param string $key |
145 | * @param mixed ...$params |
146 | * @return \Message |
147 | */ |
148 | protected function msg( $key, ...$params ) { |
149 | return $this->getMessageLocalizer()->msg( $key, ...$params ); |
150 | } |
151 | |
152 | /** |
153 | * Get the user language, or throw an exception if it has not been set. |
154 | * @return Language |
155 | */ |
156 | protected function getUserLang(): Language { |
157 | return $this->userLang; |
158 | } |
159 | |
160 | /** |
161 | * Set request dependencies |
162 | * |
163 | * @param WebRequest $request |
164 | * @param MessageLocalizer $localizer |
165 | * @param Language $userLang |
166 | */ |
167 | public function initRequest( |
168 | WebRequest $request, |
169 | MessageLocalizer $localizer, |
170 | Language $userLang |
171 | ) { |
172 | $this->request = $request; |
173 | $this->messageLocalizer = $localizer; |
174 | $this->userLang = $userLang; |
175 | } |
176 | |
177 | /** |
178 | * Called when the form is submitted. This returns a Status object which, |
179 | * when successful, contains a voting record in the value member. To |
180 | * preserve voter privacy, voting records should be the same length |
181 | * regardless of voter choices. |
182 | * @return Status |
183 | */ |
184 | public function submitForm() { |
185 | $questions = $this->election->getQuestions(); |
186 | $record = ''; |
187 | $status = new BallotStatus(); |
188 | |
189 | foreach ( $questions as $question ) { |
190 | $record .= $this->submitQuestion( $question, $status ); |
191 | } |
192 | if ( $status->isOK() ) { |
193 | $status->value = $record; |
194 | } |
195 | |
196 | return $status; |
197 | } |
198 | |
199 | /** |
200 | * Construct a string record for a given question, during form submission. |
201 | * |
202 | * If there is a problem with the form data, the function should set a |
203 | * fatal error in the $status object and return null. |
204 | * |
205 | * @param Question $question |
206 | * @param BallotStatus $status |
207 | * @return string|null |
208 | */ |
209 | abstract public function submitQuestion( $question, $status ); |
210 | |
211 | /** |
212 | * Unpack a string record into an array format suitable for the tally type |
213 | * @param string $record |
214 | * @return array|bool |
215 | */ |
216 | abstract public function unpackRecord( $record ); |
217 | |
218 | /** |
219 | * Convert a record to a string of some kind |
220 | * @param string $record |
221 | * @param array $options |
222 | * @return string[]|false |
223 | */ |
224 | public function convertRecord( $record, $options = [] ) { |
225 | $scores = $this->unpackRecord( $record ); |
226 | |
227 | return $this->convertScores( $scores ); |
228 | } |
229 | |
230 | /** |
231 | * Convert a score array to a string of some kind |
232 | * @param array $scores |
233 | * @param array $options |
234 | * @return string|string[]|false |
235 | */ |
236 | abstract public function convertScores( $scores, $options = [] ); |
237 | |
238 | /** |
239 | * Create a ballot of the given type |
240 | * @param Context $context |
241 | * @param string $type |
242 | * @param Election $election |
243 | * @return Ballot |
244 | * @throws InvalidArgumentException |
245 | */ |
246 | public static function factory( $context, $type, $election ) { |
247 | if ( !isset( self::BALLOT_TYPES[$type] ) ) { |
248 | throw new InvalidArgumentException( "Invalid ballot type: $type" ); |
249 | } |
250 | $class = self::BALLOT_TYPES[$type]; |
251 | |
252 | return new $class( $context, $election ); |
253 | } |
254 | |
255 | /** |
256 | * Constructor. |
257 | * @param Context $context |
258 | * @param Election $election |
259 | */ |
260 | public function __construct( $context, $election ) { |
261 | $this->context = $context; |
262 | $this->election = $election; |
263 | } |
264 | |
265 | /** |
266 | * Get the HTML for this ballot. <form> tags should not be included, |
267 | * they will be added by the VotePage. |
268 | * @param bool|BallotStatus $prevStatus |
269 | * @return \OOUI\Element[] |
270 | */ |
271 | public function getForm( $prevStatus = false ) { |
272 | $questions = $this->election->getQuestions(); |
273 | if ( $this->election->getProperty( 'shuffle-questions' ) ) { |
274 | shuffle( $questions ); |
275 | } |
276 | $shuffleOptions = $this->election->getProperty( 'shuffle-options' ); |
277 | $this->setErrorStatus( $prevStatus ); |
278 | |
279 | $itemArray = []; |
280 | foreach ( $questions as $question ) { |
281 | $options = $question->getOptions(); |
282 | if ( $shuffleOptions ) { |
283 | shuffle( $options ); |
284 | } |
285 | |
286 | $questionForm = $this->getQuestionForm( |
287 | $question, |
288 | $options |
289 | ); |
290 | $questionForm->setLabel( |
291 | new \OOUI\HtmlSnippet( $question->parseMessage( 'text' ) ) |
292 | ); |
293 | $itemArray[] = $questionForm; |
294 | } |
295 | if ( $prevStatus ) { |
296 | $formStatus = new \OOUI\Element( [ |
297 | 'content' => new \OOUI\HTMLSnippet( |
298 | $this->formatStatus( $prevStatus ) |
299 | ), |
300 | ] ); |
301 | array_unshift( $itemArray, $formStatus ); |
302 | } |
303 | |
304 | return $itemArray; |
305 | } |
306 | |
307 | /** |
308 | * @param bool|BallotStatus $status |
309 | */ |
310 | public function setErrorStatus( $status ) { |
311 | if ( $status ) { |
312 | $this->prevErrorIds = $status->getIds(); |
313 | $this->prevStatus = $status; |
314 | } else { |
315 | $this->prevErrorIds = []; |
316 | } |
317 | $this->usedErrorIds = []; |
318 | } |
319 | |
320 | public function errorLocationIndicator( $id ) { |
321 | if ( !isset( $this->prevErrorIds[$id] ) ) { |
322 | return ''; |
323 | } |
324 | $this->usedErrorIds[$id] = true; |
325 | |
326 | return new \OOUI\IconWidget( [ |
327 | 'icon' => 'error', |
328 | 'title' => $this->prevStatus->spGetMessageText( $id ), |
329 | 'id' => "$id-location", |
330 | 'classes' => [ 'securepoll-error-location' ], |
331 | 'flags' => 'error', |
332 | ] ); |
333 | } |
334 | |
335 | /** |
336 | * Convert a BallotStatus object to HTML |
337 | * @param BallotStatus $status |
338 | * @return string |
339 | */ |
340 | public function formatStatus( $status ) { |
341 | return $status->spGetHTML( $this->usedErrorIds ); |
342 | } |
343 | } |