Index: trunk/extensions/QPoll/i18n/qp.i18n.php |
— | — | @@ -157,18 +157,32 @@ |
158 | 158 | 'pollresults' => 'Special page name in [[Special:SpecialPages]]', |
159 | 159 | 'qpollwebinstall' => 'Special page name in [[Special:SpecialPages]]', |
160 | 160 | 'qp_desc' => '{{desc}} Important notice: Categories can be grouped into category groups, which are internally referred as "spans". Such grouped categories become "subcategories". While the extension evolved, these groups were consequentially called as "spans", "metacategories", "category groups". Please read the on-line documentation carefully before translating.', |
| 161 | + 'qp_desc-sp' => 'Description of extension\'s special page at Special:Version page.', |
| 162 | + 'qp_result_NA' => '{{Identical|Not answered}}', |
161 | 163 | 'qp_result_error' => '{{Identical|Syntax error}}', |
162 | 164 | 'qp_vote_button' => '{{Identical|Vote}}', |
| 165 | + 'qp_vote_again_button' => 'When the user already submitted the poll / quiz, text of submit button title changes to indicate that the data will be re-submitted again.', |
| 166 | + 'qp_submit_attempts_left' => 'How many attempts of poll / quiz submissions are left to the current user.', |
| 167 | + 'qp_polls_list' => 'A link title in the header of [[Special:Pollresults]] which displays list of polls at current wiki site.', |
| 168 | + 'qp_users_list' => 'A link title in the header of [[Special:Pollresults]] which displays list of poll participants at current wiki site.', |
| 169 | + 'qp_browse_to_poll' => 'A link title at [[Special:Pollresults]] which opens wiki page where the selected poll was defined.', |
| 170 | + 'qp_browse_to_user' => 'A link title at [[Special:Pollresults]] which opens user page of selected poll\'s voter (participant).', |
| 171 | + 'qp_browse_to_interpretation' => 'A link title at [[Special:Pollresults]] which opens wiki page where the interpretation script of selected poll was defined.', |
| 172 | + 'qp_votes_count' => 'Displays how many times the user re-submitted the poll. $1 is number of poll submissions.', |
163 | 173 | 'qp_source_link' => '"Source" is the link text for a link to the page where the poll is defined. |
164 | 174 | {{Identical|Source}}', |
165 | 175 | 'qp_stats_link' => '{{Identical|Statistics}}', |
166 | 176 | 'qp_users_link' => '{{Identical|User}}', |
| 177 | + 'qp_voice_link' => 'A link title at [[Special:Pollresults]] which displays user vote (selected categories) for the selected poll.', |
167 | 178 | 'qp_voice_link_inv' => 'At the moment of query generation there was no user answer for the selected poll yet. Question mark encourages wiki administrator to re-submit the query again.', |
168 | 179 | 'qp_user_polls_link' => 'Parameters: |
169 | 180 | * $1 is the number of polls participated in. |
170 | 181 | * $2 is the name of the user this message refers to (optional - use for GENDER)', |
171 | 182 | 'qp_user_missing_polls_link' => 'Parameters: |
172 | 183 | * $1 is the name of the user this message refers to (optional - use for GENDER)', |
| 184 | + 'qp_not_participated_link' => 'A link title at [[Special:Pollresults]] which displays the list of users which participated in another polls but did not participate in the selected poll.', |
| 185 | + 'qp_order_by_username' => 'Order the list of polls at [[Special:Pollresults]] by username.', |
| 186 | + 'qp_order_by_polls_count' => 'Order the list of polls at [[Special:Pollresults]] by number of their voters (popularity of the polls).', |
173 | 187 | 'qp_results_line_qupl' => 'Parameters: |
174 | 188 | * $1 is a link to the page page name the poll is on with the page title as link label |
175 | 189 | * $2 is the poll name in plain text |
— | — | @@ -180,18 +194,52 @@ |
181 | 195 | * $4 is a link to the poll statistics with link label {{msg-mw|qp_stats_link}} |
182 | 196 | * $5 is a link to the users that participated in the poll with link label {{msg-mw|qp_users_link}} |
183 | 197 | * $6 is a link to the with link label {{msg-mw|qp_not_participated_link}}', |
| 198 | + 'qp_header_line_qpul' => 'Parameters: |
| 199 | +* $1 is a link to the [[Special:Pollresults]] which displays either the list of users which participated in current poll, or the list of users that participated in another polls but this one |
| 200 | +* $2 is a link to the page title where the poll is defined |
| 201 | +* $3 is the poll name (poll identifier) in plain text', |
| 202 | + 'qp_results_line_qpl' => 'Parameters: |
| 203 | +* $1 is a link to the page page name the poll is on with the page title as link label |
| 204 | +* $2 is the poll name in plain text |
| 205 | +* $3 is a link to the poll with link label {{msg-mw|qp_source_link}} |
| 206 | +* $4 is a link to the poll statistics with link label {{msg-mw|qp_stats_link}} |
| 207 | +* $5 is a link to the users that participated in the poll with link label {{msg-mw|qp_users_link}} |
| 208 | +* $6 is a link to the with link label {{msg-mw|qp_not_participated_link}}', |
184 | 209 | 'qp_results_submit_attempts' => 'Parameters: |
185 | 210 | * $1 is the number of submit attempts', |
186 | | - 'qp_results_short_interpretation' => 'No parameters', |
187 | | - 'qp_results_long_interpretation' => 'No parameters', |
| 211 | + 'qp_results_interpretation_header' => 'Since v0.8.0 polls may have interpretation scripts defined at separate wiki pages, which allows to use the extension for quizes. Interpretation scripts return the following types of interpretation results: global error message, proposal error, proposal/category error, short interpretation, long interpretation, structured interpretation. This message is the header of the block of these results displayed both to end-user and to poll admin at [[Special:Pollresults]] page.', |
| 212 | + 'qp_results_short_interpretation' => 'Short interpretation header. Short interpretation will contain small sortable string in separate block. No parameters.', |
| 213 | + 'qp_results_long_interpretation' => 'Long interpretation header. Long interpretation will contain long text (usually displayed to end-user) in separate block. No parameters.', |
| 214 | + 'qp_results_structured_interpretation' => 'Structured interpretation header. This type of interpretation is used to store measurable accountable structured data in interpretation scripts. Structured interpretation also can be read by another poll\'s interpretation scripts at later time and exported into XLS format. No parameters.', |
188 | 215 | 'qp_error_missed_dependance_poll' => 'Parameters: |
189 | 216 | * $1 is the poll ID of the poll having an error. |
190 | 217 | * $2 is a link to the page with the poll, that this erroneous poll depends on. |
191 | 218 | * $3 is the poll ID of the poll, which this erroneous poll depends on.', |
192 | 219 | 'qp_error_too_many_spans' => 'There cannot be more category groups defined than the total count of subcategories.', |
193 | 220 | 'qp_error_too_few_spans' => 'Every category group should include at least two subcategories', |
194 | | - 'qp_error_no_interpretation' => 'Title of interpretation script was specified in poll header, however no article was found with that title. Either remove "interpretation" xml attribute of poll or create the title specified by "interpretation" attribute.', |
| 221 | + 'qp_error_no_interpretation' => 'Title of interpretation script was specified in poll header, but no article was found with that title. Either remove "interpretation" xml attribute of poll or create the title specified by "interpretation" attribute.', |
| 222 | + 'qp_interpetation_wrong_answer' => 'Indicates that user-submitted choices of categories are considered incorrect by poll\'s interpretation script.', |
| 223 | + 'qp_export_to_xls' => 'Poll average statistics will be exported into XLS file.', |
| 224 | + 'qp_voices_to_xls' => 'Poll user category choices will be exported into XLS file.', |
| 225 | + 'qp_interpretation_results_to_xls' => 'Poll\`s structured interpetation results will be exported into XLS file.', |
| 226 | + 'qp_users_answered_questions' => 'Writes the message how many users answered to the poll questions in XLS file cell.', |
| 227 | + 'qp_func_no_such_poll' => 'qpuserchoice parser function did not found the poll specified in it\'s parameter. |
| 228 | +* $1 is the poll address (page title + hash sign + qp prefix + poll id)', |
| 229 | + 'qp_func_missing_question_id' => 'qpuserchoice parser function call should have question_id parameter specified.', |
| 230 | + 'qp_func_invalid_question_id' => 'qpuserchoice parser function question_id call value should be integer number, [1..n].', |
| 231 | + 'qp_func_missing_proposal_id' => 'qpuserchoice parser function call should have proposal id parameter specified.', |
| 232 | + 'qp_func_invalid_proposal_id' => 'qpuserchoice parser function proposal id call value should be integer number, [0..n].', |
| 233 | + 'qp_error_no_such_poll' => 'Poll statistics cannot be displayed because incorrect / missing poll address was specified in qpoll tag statistical mode.', |
| 234 | + 'qp_error_in_question_header' => 'Main question header (common question or xml-like attributes) has syntax error. |
| 235 | +* $1 is the source text of header.', |
| 236 | + 'qp_error_id_in_stats_mode' => 'Poll "id" attribute is meaningless in statistical display mode.', |
| 237 | + 'qp_error_dependance_in_stats_mode' => 'Poll "dependance" attribute is meaningless in statistical display mode.', |
| 238 | + 'qp_error_address_in_decl_mode' => 'Poll "address" attribute is meaningless in poll declaration / voting mode.', |
| 239 | + 'qp_error_question_not_implemented' => 'Invalid value of qustion xml-like "type" attribute was specified. There is no such type of question. Please read the manual for list of valid question types.', |
| 240 | + 'qp_error_invalid_question_type' => '{{Identical|Invalid value of qustion xml-like "type" attribute was specified. There is no such type of question. Please read the manual for list of valid question types.}}', |
195 | 241 | 'qp_error_interpretation_no_return' => 'Interpretation script missed an return statement.', |
| 242 | + 'qp_error_type_in_stats_mode' => 'Question\'s "type" xml-like attribute is meaningless in statistical display mode.', |
| 243 | + 'qp_error_no_poll_id' => 'Every poll definition in declaration / voting mode must have "id" attribute.', |
196 | 244 | 'qp_error_structured_interpretation_is_too_long' => "Structured interpretation is serialized string containing scalar value or an associative array stored into database table field. It's purpose is to have measurable, easily processable interpretation result for the particular poll which then can be processed by external tools (via XLS export) or, to be read and processed by next poll interpretation script (data import and in the future maybe an export as well). When the serialized string is too long, it should never be stored, otherwise it will be truncated by DBMS so it cannot be properly unserialized later.", |
197 | 245 | 'qp_error_eval_missed_lang_attr' => '{{doc-important|Do not translate "lang" as it is the name of an XML attribute that is not localised.}}', |
198 | 246 | 'qp_error_eval_mix_languages' => 'Parameters: |
Index: trunk/extensions/QPoll/ctrl/question/qp_textquestion.php |
— | — | @@ -161,6 +161,24 @@ |
162 | 162 | # regexp for separation of proposal line tokens |
163 | 163 | static $propCatPattern = null; |
164 | 164 | |
| 165 | + # source "raw" tokens (preg_split) |
| 166 | + var $rawtokens; |
| 167 | + |
| 168 | + /** |
| 169 | + * array with parsed braces pairs |
| 170 | + * every element may have the following keys: |
| 171 | + * 'type' : type of brace string _key_values_ of self::$matching_braces |
| 172 | + * 'closed_at' : indicates opening brace; |
| 173 | + * false when no matching closing brace was found |
| 174 | + * int key of $this->rawtokens a "link" to matching closing brace |
| 175 | + * 'opened_at' : indicates closing brace; |
| 176 | + * false when no matching opening brace was found |
| 177 | + * int key of $this->rawtokens a "link" to matching opening brace |
| 178 | + * 'iscat' : indicates brace that belongs to self::$input_braces_types AND |
| 179 | + * has a proper match (both 'closed_at' and 'opened_at' are int) |
| 180 | + */ |
| 181 | + var $brace_matches; |
| 182 | + |
165 | 183 | # $propview is an instance of qp_TextQuestionProposalView |
166 | 184 | # which contains parsed tokens for combined |
167 | 185 | # proposal/category view |
— | — | @@ -258,6 +276,111 @@ |
259 | 277 | } |
260 | 278 | |
261 | 279 | /** |
| 280 | + * Builds $this->brace_matches array which contains the list of matching braces |
| 281 | + * from $this->rawtokens array. |
| 282 | + */ |
| 283 | + private function findMatchingBraces() { |
| 284 | + $brace_stack = array(); |
| 285 | + $this->brace_matches = array(); |
| 286 | + $matching_closed_brace = ''; |
| 287 | + # building $this->brace_matches |
| 288 | + foreach ( $this->rawtokens as $tkey => $token ) { |
| 289 | + if ( array_key_exists( $token, self::$matching_braces ) ) { |
| 290 | + # opening braces |
| 291 | + $this->brace_matches[$tkey] = array( |
| 292 | + 'closed_at' => false, |
| 293 | + 'type' => $token |
| 294 | + ); |
| 295 | + $match = self::$matching_braces[$token]; |
| 296 | + # create new brace_stack element: |
| 297 | + $last_brace_def = array( |
| 298 | + 'match' => $match, |
| 299 | + 'idx' => $tkey |
| 300 | + ); |
| 301 | + if ( array_key_exists( $token, self::$input_braces_types ) && |
| 302 | + count( $brace_stack ) == 0 ) { |
| 303 | + # will try to start category definiton (on closing) |
| 304 | + $matching_closed_brace = $match; |
| 305 | + } |
| 306 | + array_push( $brace_stack, $last_brace_def ); |
| 307 | + } elseif ( in_array( $token, self::$matching_braces ) ) { |
| 308 | + # closing braces |
| 309 | + $this->brace_matches[$tkey] = array( |
| 310 | + 'opened_at' => false, |
| 311 | + # we always put opening brace in 'type' |
| 312 | + 'type' => array_search( $token, self::$matching_braces, true ) |
| 313 | + ); |
| 314 | + if ( count( $brace_stack ) > 0 ) { |
| 315 | + $last_brace_def = array_pop( $brace_stack ); |
| 316 | + if ( $last_brace_def['match'] != $token ) { |
| 317 | + # braces didn't match |
| 318 | + array_push( $brace_stack, $last_brace_def ); |
| 319 | + continue; |
| 320 | + } |
| 321 | + $idx = $last_brace_def['idx']; |
| 322 | + # link opening / closing braces to each other |
| 323 | + $this->brace_matches[$tkey]['opened_at'] = $idx; |
| 324 | + $this->brace_matches[$idx]['closed_at'] = $tkey; |
| 325 | + if ( count( $brace_stack ) > 0 || $token !== $matching_closed_brace ) { |
| 326 | + # brace does not belong to self::$input_braces_types |
| 327 | + continue; |
| 328 | + } |
| 329 | + # stack level 1 and found a matching_closed_brace; |
| 330 | + # indicate end of category in $this->brace_matches |
| 331 | + $this->brace_matches[$tkey]['iscat'] = true; |
| 332 | + # indicate begin of category in $this->brace_matches |
| 333 | + $this->brace_matches[$idx]['iscat'] = true; |
| 334 | + # clear match |
| 335 | + $matching_closed_brace = ''; |
| 336 | + } |
| 337 | + } |
| 338 | + } |
| 339 | + # trying to backtrack non-closed braces only these which belong to |
| 340 | + # self::$input_braces_types |
| 341 | + $brace_keys = array_keys( $this->brace_matches, true ); |
| 342 | + for ( $i = count( $brace_keys ) - 1; $i >= 0; $i-- ) { |
| 343 | + $brace_match = &$this->brace_matches[$brace_keys[$i]]; |
| 344 | + # match non-closed brace which belongs to self::$input_braces_types |
| 345 | + # (non-closed category definitions) |
| 346 | + if ( array_key_exists( 'opened_at', $brace_match ) && |
| 347 | + $brace_match['opened_at'] === false && |
| 348 | + array_key_exists( $brace_match['type'], self::$input_braces_types ) ) { |
| 349 | + # try to find matching opening brace for current non-closed closing brace |
| 350 | + for ( $j = $i - 1; $j >= 0; $j-- ) { |
| 351 | + $checked_brace = &$this->brace_matches[$brace_keys[$j]]; |
| 352 | + if ( array_key_exists( 'iscat', $checked_brace ) ) { |
| 353 | + # category definitions cannot be nested |
| 354 | + break; |
| 355 | + } |
| 356 | + if ( array_key_exists( 'closed_at', $checked_brace ) ) { |
| 357 | + # opening brace |
| 358 | + if ( $checked_brace['closed_at'] === false ) { |
| 359 | + # opening brace that has no matching closing brace |
| 360 | + if ( $checked_brace['type'] === $brace_match['type'] ) { |
| 361 | + # opening brace of the same type that $brace_match have |
| 362 | + # link $brace_match and $checked_brace to each other: |
| 363 | + # found matching non-closed opening brace; "link" both to each other |
| 364 | + $brace_match['opened_at'] = $brace_keys[$j]; |
| 365 | + $brace_match['iscat'] = true; |
| 366 | + $checked_brace['closed_at'] = $brace_keys[$i]; |
| 367 | + $checked_brace['iscat'] = true; |
| 368 | + break; |
| 369 | + } |
| 370 | + } elseif ( $checked_brace['closed_at'] > $i ) { |
| 371 | + # found opening brace that is closed at higher position than our |
| 372 | + # $brace_match has; |
| 373 | + # cross-closing is not allowed, the following code cannot be |
| 374 | + # category definition and template at the same time: |
| 375 | + # "<[ {{ ]> }}" |
| 376 | + break; |
| 377 | + } |
| 378 | + } |
| 379 | + } |
| 380 | + } |
| 381 | + } |
| 382 | + } |
| 383 | + |
| 384 | + /** |
262 | 385 | * Creates question view which should be renreded and |
263 | 386 | * also may be altered during the poll generation |
264 | 387 | */ |
— | — | @@ -275,44 +398,41 @@ |
276 | 399 | $this->dbtokens = $brace_stack = array(); |
277 | 400 | $catId = 0; |
278 | 401 | $last_brace = ''; |
279 | | - $tokens = preg_split( self::$propCatPattern, $raw, -1, PREG_SPLIT_DELIM_CAPTURE ); |
| 402 | + $this->rawtokens = preg_split( self::$propCatPattern, $raw, -1, PREG_SPLIT_DELIM_CAPTURE ); |
280 | 403 | $matching_closed_brace = ''; |
281 | | - foreach ( $tokens as $token ) { |
282 | | - try { |
283 | | - # $toBeStored == true when current $token has to be stored into |
284 | | - # category / proposal list (depending on $opt->isCatDef) |
285 | | - $toBeStored = true; |
286 | | - if ( $token === '|' ) { |
287 | | - # parameters separator |
288 | | - if ( $opt->isCatDef ) { |
289 | | - if ( count( $brace_stack ) == 1 && $brace_stack[0] === $matching_closed_brace ) { |
290 | | - # pipe char starts new option only at top brace level, |
291 | | - # with matching input brace |
292 | | - $opt->addEmptyOption(); |
293 | | - $toBeStored = false; |
294 | | - } |
| 404 | + $this->findMatchingBraces(); |
| 405 | + foreach ( $this->rawtokens as $tkey => $token ) { |
| 406 | + # $toBeStored == true when current $token has to be stored into |
| 407 | + # category / proposal list (depending on $opt->isCatDef) |
| 408 | + $toBeStored = true; |
| 409 | + if ( $token === '|' ) { |
| 410 | + # parameters separator |
| 411 | + if ( $opt->isCatDef ) { |
| 412 | + if ( count( $brace_stack ) == 1 && $brace_stack[0] === $matching_closed_brace ) { |
| 413 | + # pipe char starts new option only at top brace level, |
| 414 | + # with matching input brace |
| 415 | + $opt->addEmptyOption(); |
| 416 | + $toBeStored = false; |
295 | 417 | } |
296 | | - } elseif ( array_key_exists( $token, self::$matching_braces ) ) { |
297 | | - # opening braces |
| 418 | + } |
| 419 | + } elseif ( array_key_exists( $tkey, $this->brace_matches ) ) { |
| 420 | + # brace |
| 421 | + $brace_match = &$this->brace_matches[$tkey]; |
| 422 | + if ( array_key_exists( 'closed_at', $brace_match ) && |
| 423 | + $brace_match['closed_at'] !== false ) { |
| 424 | + # valid opening brace |
298 | 425 | array_push( $brace_stack, self::$matching_braces[$token] ); |
299 | | - if ( array_key_exists( $token, self::$input_braces_types ) && |
300 | | - count( $brace_stack ) == 1 ) { |
301 | | - # start category definiton |
| 426 | + if ( array_key_exists( 'iscat', $brace_match ) ) { |
| 427 | + # start category definition |
302 | 428 | $matching_closed_brace = self::$matching_braces[$token]; |
303 | 429 | $opt->startOptionsList( self::$input_braces_types[$token] ); |
304 | 430 | $toBeStored = false; |
305 | 431 | } |
306 | | - } elseif ( in_array( $token, self::$matching_braces ) ) { |
307 | | - # closing braces |
308 | | - if ( count( $brace_stack ) > 0 ) { |
309 | | - $last_brace = array_pop( $brace_stack ); |
310 | | - if ( $last_brace != $token ) { |
311 | | - array_push( $brace_stack, $last_brace ); |
312 | | - throw new Exception( 'break' ); |
313 | | - } |
314 | | - if ( count( $brace_stack ) > 0 || $token !== $matching_closed_brace ) { |
315 | | - throw new Exception( 'break' ); |
316 | | - } |
| 432 | + } elseif ( array_key_exists( 'opened_at', $brace_match ) && |
| 433 | + $brace_match['opened_at'] !== false ) { |
| 434 | + # valid closing brace |
| 435 | + array_pop( $brace_stack ); |
| 436 | + if ( array_key_exists( 'iscat', $brace_match ) ) { |
317 | 437 | $matching_closed_brace = ''; |
318 | 438 | # add new category input options for the storage |
319 | 439 | $this->dbtokens[] = $opt->input_options; |
— | — | @@ -325,10 +445,6 @@ |
326 | 446 | $toBeStored = false; |
327 | 447 | } |
328 | 448 | } |
329 | | - } catch ( Exception $e ) { |
330 | | - if ( $e->getMessage() !== 'break' ) { |
331 | | - throw new MWException( $e->getMessage() ); |
332 | | - } |
333 | 449 | } |
334 | 450 | if ( $toBeStored ) { |
335 | 451 | if ( $opt->isCatDef ) { |