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