Index: trunk/extensions/QPoll/maintenance/qp_schemaupdater.php |
— | — | @@ -34,6 +34,7 @@ |
35 | 35 | array( 'Title' => 'newFromID' ), |
36 | 36 | array( 'WebResponse' => 'setCookie' ), |
37 | 37 | array( 'Language' => 'lc' ), |
| 38 | + array( 'Language' => 'truncate' ), |
38 | 39 | array( 'User' => 'isAnon' ) |
39 | 40 | ); |
40 | 41 | |
Index: trunk/extensions/QPoll/clientside/qp_user.css |
— | — | @@ -1,4 +1,5 @@ |
2 | 2 | .qpoll .error { background-color: LightYellow; } |
| 3 | +.qpoll .object_error { background-color: #D700D7; } |
3 | 4 | .qpoll .fatalerror { border: 1px solid gray; padding: 4px; background-color: LightYellow; } |
4 | 5 | .qpoll .settings input.numerical { width:2em; } |
5 | 6 | .qpoll .header {font-weight: bold;} |
— | — | @@ -37,7 +38,7 @@ |
38 | 39 | .qpoll .interp_answer { border: 2px solid gray; padding: 0.5em; color: black; background-color: lightblue; } |
39 | 40 | .qpoll .cat_prefilled { color: blue; } |
40 | 41 | .qpoll .cat_noanswer { background-color: LightYellow; } |
41 | | -.qpoll .cell_error { background-color: #D700D7; } |
| 42 | +.qpoll .cat_error { background-color: LightYellow; border: 1px solid gray; } |
42 | 43 | |
43 | 44 | /* script view */ |
44 | 45 | .qpoll .line_numbers { font-family: monospace, "Courier New"; white-space:pre; color:green; background-color: lightgray; float:left; border-left: 1px solid darkgray; border-top: 1px solid darkgray; border-bottom: 1px solid darkgray; padding: 0.5em; } |
Index: trunk/extensions/QPoll/qp_interpret.php |
— | — | @@ -109,12 +109,22 @@ |
110 | 110 | if ( !is_array( $result ) ) { |
111 | 111 | return $interpResult->setError( wfMsg( 'qp_error_interpretation_no_return' ) ); |
112 | 112 | } |
113 | | - # initialize $interpResult->qpError[] member array |
| 113 | + # initialize $interpResult->qpcErrors[] member array |
114 | 114 | foreach ( $result as $qidx => $question ) { |
115 | 115 | if ( is_int( $qidx ) && is_array( $question ) ) { |
116 | | - foreach ( $question as $pidx => $error ) { |
| 116 | + foreach ( $question as $pidx => $prop_error ) { |
117 | 117 | if ( is_int( $pidx ) ) { |
118 | | - $interpResult->setQPerror( $qidx, $pidx, $error ); |
| 118 | + if ( is_array( $prop_error ) ) { |
| 119 | + # separate error messages list for proposal categories |
| 120 | + foreach ( $prop_error as $cidx => $cat_error ) { |
| 121 | + if ( is_int( $cidx ) ) { |
| 122 | + $interpResult->setQPCerror( $cat_error, $qidx, $pidx, $cidx ); |
| 123 | + } |
| 124 | + } |
| 125 | + } else { |
| 126 | + # error message for the whole proposal line |
| 127 | + $interpResult->setQPCerror( $prop_error, $qidx, $pidx ); |
| 128 | + } |
119 | 129 | } |
120 | 130 | } |
121 | 131 | } |
Index: trunk/extensions/QPoll/ctrl/qp_abstractpoll.php |
— | — | @@ -91,18 +91,19 @@ |
92 | 92 | * |
93 | 93 | * @public |
94 | 94 | */ |
95 | | - function __construct( $argv, qp_AbstractView $view ) { |
| 95 | + function __construct( $argv, qp_AbstractPollView $view ) { |
96 | 96 | global $wgRequest, $wgLanguageCode; |
97 | 97 | $this->mRequest = &$wgRequest; |
98 | 98 | $this->mResponse = $wgRequest->response(); |
99 | 99 | # Determine which messages will be used, according to the language. |
100 | 100 | qp_Setup::onLoadAllMessages(); |
| 101 | + $view->setController( $this ); |
101 | 102 | # *** get visual style poll attributes *** |
102 | 103 | $perRow = intval( array_key_exists( 'perrow', $argv ) ? $argv['perrow'] : 1 ); |
103 | 104 | if ( $perRow < 1 ) { |
104 | 105 | $perRow = 1; |
105 | 106 | } |
106 | | - $view->setController( $this, $perRow ); |
| 107 | + $view->setPerRow( $perRow ); |
107 | 108 | $this->view = $view; |
108 | 109 | # reset the unique index number of the question in the current poll (used to instantiate the questions) |
109 | 110 | $this->mQuestionId = 0; // ( correspons to 'question_id' DB field ) |
Index: trunk/extensions/QPoll/ctrl/qp_textquestion.php |
— | — | @@ -175,11 +175,11 @@ |
176 | 176 | $proposalId = 0; |
177 | 177 | # Currently, we use just a single instance (no nested categories) |
178 | 178 | $opt = new qp_TextQuestionOptions(); |
179 | | - $this->viewtokens = new qp_TextQuestionViewTokens( $this->view ); |
180 | 179 | foreach ( $this->raws as $raw ) { |
181 | 180 | $this->view->initProposalView(); |
182 | 181 | $opt->reset(); |
183 | | - $this->viewtokens->reset(); |
| 182 | + $this->viewtokens = new qp_TextQuestionViewTokens(); |
| 183 | + # $this->viewtokens->reset(); |
184 | 184 | $this->dbtokens = $brace_stack = array(); |
185 | 185 | $catId = 0; |
186 | 186 | $last_brace = ''; |
— | — | @@ -244,13 +244,13 @@ |
245 | 245 | # check if there is at least one category defined |
246 | 246 | if ( $catId === 0 ) { |
247 | 247 | # todo: this is the explanary line, it is not real proposal |
248 | | - $this->viewtokens->prependErrorToken( wfMsg( 'qp_error_too_few_categories' ), 'error' ); |
| 248 | + $this->viewtokens->prependErrorToken( $this->view->bodyErrorMessage( wfMsg( 'qp_error_too_few_categories' ), 'error' ) ); |
249 | 249 | } |
250 | 250 | if ( strlen( $proposal_text = serialize( $this->dbtokens ) ) > qp_Setup::$proposal_max_length ) { |
251 | 251 | # too long proposal field to store into the DB |
252 | 252 | # this is very important check for text questions because |
253 | 253 | # category definitions are stored within the proposal text |
254 | | - $this->viewtokens->prependErrorToken( wfMsg( 'qp_error_too_long_proposal_text' ), 'error' ); |
| 254 | + $this->viewtokens->prependErrorToken( $this->view->bodyErrorMessage( wfMsg( 'qp_error_too_long_proposal_text' ), 'error' ) ); |
255 | 255 | } |
256 | 256 | $this->mProposalText[$proposalId] = $proposal_text; |
257 | 257 | if ( $this->poll->mBeingCorrected ) { |
— | — | @@ -269,11 +269,11 @@ |
270 | 270 | } catch ( Exception $e ) { |
271 | 271 | if ( $e->getMessage() == 'qp_error' ) { |
272 | 272 | $prev_state = $this->getState(); |
273 | | - $this->viewtokens->prependErrorToken( wfMsg( 'qp_error_no_answer' ), 'NA' ); |
| 273 | + $this->viewtokens->prependErrorToken( $this->view->bodyErrorMessage( wfMsg( 'qp_error_no_answer' ), 'NA' ) ); |
274 | 274 | } |
275 | 275 | } |
276 | 276 | } |
277 | | - $this->view->addProposal( $proposalId, $this->viewtokens->tokenslist ); |
| 277 | + $this->view->addProposal( $proposalId, $this->viewtokens ); |
278 | 278 | $proposalId++; |
279 | 279 | } |
280 | 280 | } |
Index: trunk/extensions/QPoll/ctrl/qp_tabularquestion.php |
— | — | @@ -17,7 +17,7 @@ |
18 | 18 | * @param $view an instance of question view "linked" to this question |
19 | 19 | * @param $questionId the identifier of the question used to generate input names |
20 | 20 | */ |
21 | | - function __construct( qp_AbstractPoll $poll, qp_AbstractView $view, $questionId ) { |
| 21 | + function __construct( qp_AbstractPoll $poll, qp_StubQuestionView $view, $questionId ) { |
22 | 22 | parent::__construct( $poll, $view, $questionId ); |
23 | 23 | $this->mProposalPattern = '`^[^\|\!].*`u'; |
24 | 24 | $this->mCategoryPattern = '`^\|(\n|[^\|].*\n)`u'; |
Index: trunk/extensions/QPoll/ctrl/qp_stubquestion.php |
— | — | @@ -26,7 +26,7 @@ |
27 | 27 | * @param $view an instance of question view "linked" to this question |
28 | 28 | * @param $questionId the identifier of the question used to generate input names |
29 | 29 | */ |
30 | | - function __construct( qp_AbstractPoll $poll, qp_AbstractView $view, $questionId ) { |
| 30 | + function __construct( qp_AbstractPoll $poll, qp_StubQuestionView $view, $questionId ) { |
31 | 31 | parent::__construct( $poll, $view, $questionId ); |
32 | 32 | } |
33 | 33 | |
— | — | @@ -113,16 +113,17 @@ |
114 | 114 | } |
115 | 115 | |
116 | 116 | /** |
117 | | - * @return associative array of script-generated error messages for current question proposals |
| 117 | + * @return associative array of script-generated interpretation error |
| 118 | + * messages for current question proposals (and optionally categories) |
118 | 119 | * false, when there are no script-generated error messages |
119 | 120 | */ |
120 | | - function getProposalsErrors() { |
| 121 | + function getInterpErrors() { |
121 | 122 | $interpResult = &$this->poll->pollStore->interpResult; |
122 | | - if ( !is_array( $interpResult->qpErrors ) || |
123 | | - !isset( $interpResult->qpErrors[$this->mQuestionId] ) ) { |
| 123 | + if ( !is_array( $interpResult->qpcErrors ) || |
| 124 | + !isset( $interpResult->qpcErrors[$this->mQuestionId] ) ) { |
124 | 125 | return false; |
125 | 126 | } |
126 | | - return $interpResult->qpErrors[$this->mQuestionId]; |
| 127 | + return $interpResult->qpcErrors[$this->mQuestionId]; |
127 | 128 | } |
128 | 129 | |
129 | 130 | /** |
Index: trunk/extensions/QPoll/ctrl/qp_poll.php |
— | — | @@ -218,7 +218,7 @@ |
219 | 219 | $this->pollStore->setUserVote(); |
220 | 220 | } |
221 | 221 | if ( $this->pollStore->interpResult->isError() ) { |
222 | | - # no redirect when there are script-generated proposal errors (quiz mode) |
| 222 | + # no redirect when there are script-generated interpretation errors (quiz mode) |
223 | 223 | return false; |
224 | 224 | } |
225 | 225 | if ( $this->pollStore->voteDone ) { |
Index: trunk/extensions/QPoll/qp_user.php |
— | — | @@ -202,6 +202,12 @@ |
203 | 203 | # it is important only for question type="text", where |
204 | 204 | # proposal text contains serialized array of proposal parts and category fields |
205 | 205 | public static $proposal_max_length = 768; |
| 206 | + # whether to show short, long, serialized interpretation results to end user |
| 207 | + public static $show_interpretation = array( |
| 208 | + 'short' => false, |
| 209 | + 'long' => true, |
| 210 | + 'serialized' => false |
| 211 | + ); |
206 | 212 | /* end of default configuration settings */ |
207 | 213 | |
208 | 214 | static function entities( $s ) { |
— | — | @@ -213,33 +219,6 @@ |
214 | 220 | } |
215 | 221 | |
216 | 222 | /** |
217 | | - * Limit the maximum length of proposal line with respect to UTF-8 character bounds |
218 | | - * |
219 | | - * Question type=text proposal lengths are additionally checked in the question controller |
220 | | - * because these are stored in serialized format. |
221 | | - * Questions of another types have their proposal texts optionally cut down, because |
222 | | - * the whole text of proposal is not important. |
223 | | - * |
224 | | - * @param $ptext string proposal text |
225 | | - * @return string proposal text either cut down or unaltered |
226 | | - */ |
227 | | - private function limitProposalLength( $ptext ) { |
228 | | - if ( strlen( $ptext ) <= self::$proposal_max_length ) { |
229 | | - return $ptext; |
230 | | - } |
231 | | - for ( $curr_len = self::$proposal_max_length;/* noop */;$curr_len-- ) { |
232 | | - $pcut = substr( $ptext, 0, $curr_len ); |
233 | | - if ( mb_substr( $ptext, 0, mb_strlen( $pcut, 'UTF-8' ), 'UTF-8' ) === $pcut ) { |
234 | | - # valid UTF-8 cut |
235 | | - break; |
236 | | - } |
237 | | - # will decrease the $curr_len until valid cut is achieved; |
238 | | - # should not be more than 5 iterations, very often 1..3 |
239 | | - } |
240 | | - return $pcut; |
241 | | - } |
242 | | - |
243 | | - /** |
244 | 223 | * Autoload classes from the map provided |
245 | 224 | */ |
246 | 225 | static function autoLoad( $map ) { |
— | — | @@ -308,6 +287,7 @@ |
309 | 288 | ## isCompatibleController() method is used to check linked controllers (bugcheck) |
310 | 289 | # generic |
311 | 290 | 'view/qp_abstractview.php' => 'qp_AbstractView', |
| 291 | + 'view/qp_abstractpollview.php' => 'qp_AbstractPollView', |
312 | 292 | # questions |
313 | 293 | 'view/qp_stubquestionview.php' => 'qp_StubQuestionView', |
314 | 294 | 'view/qp_tabularquestionview.php' => 'qp_TabularQuestionView', |
Index: trunk/extensions/QPoll/qp_pollstore.php |
— | — | @@ -9,16 +9,24 @@ |
10 | 10 | */ |
11 | 11 | class qp_InterpResult { |
12 | 12 | # short answer. it is supposed to be sortable and accountable in statistics |
| 13 | + # by default, it is private (displayed only in Special:Pollresults page) |
13 | 14 | # blank value means short answer is unavailable |
14 | 15 | var $short = ''; |
15 | 16 | # long answer. it is supposed to be understandable by amateur users |
| 17 | + # by default, it is public (displayed everywhere) |
16 | 18 | # blank value means long answer is unavailable |
17 | 19 | var $long = ''; |
| 20 | + # serialized answer. one dimensional array with numeric keys and scalar values. |
| 21 | + # it is exported to XLS voices and can be analyzed by external tools. |
| 22 | + var $serialized = ''; |
18 | 23 | # error message. non-blank value indicates interpretation script error |
19 | 24 | # either due to incorrect script code, or a script-generated one |
20 | 25 | var $error = ''; |
21 | | - # 2d array of errors generated for [question][proposal], false if no errors |
22 | | - var $qpErrors = false; |
| 26 | + # interpretation result |
| 27 | + # 2d array of errors generated for [question][proposal] |
| 28 | + # 3d array of errors generated for [question][proposal][category] |
| 29 | + # false if no errors |
| 30 | + var $qpcErrors = false; |
23 | 31 | |
24 | 32 | /** |
25 | 33 | * @param $init - optional array of properties to be initialized |
— | — | @@ -46,29 +54,43 @@ |
47 | 55 | /** |
48 | 56 | * set question / proposal error message (for quizes) |
49 | 57 | * |
| 58 | + * @param $msg string error message for [question][proposal] pair; |
| 59 | + * non-string for default message |
50 | 60 | * @param $qidx int index of poll's question |
51 | 61 | * @param $pidx int index of question's proposal |
52 | | - * @param $msg string error message for [question][proposal] pair |
| 62 | + * @param $cidx int index of proposal's category (optional) |
53 | 63 | */ |
54 | | - function setQPerror( $qidx, $pidx, $msg ) { |
55 | | - if ( !is_array( $this->qpErrors ) ) { |
56 | | - $this->qpErrors = array(); |
| 64 | + function setQPCerror( $msg, $qidx, $pidx, $cidx = null ) { |
| 65 | + if ( !is_array( $this->qpcErrors ) ) { |
| 66 | + $this->qpcErrors = array(); |
57 | 67 | } |
58 | | - if ( !isset( $this->qpErrors[$qidx] ) ) { |
59 | | - $this->qpErrors[$qidx] = array(); |
| 68 | + if ( !array_key_exists( $qidx, $this->qpcErrors ) ) { |
| 69 | + $this->qpcErrors[$qidx] = array(); |
60 | 70 | } |
61 | | - $this->qpErrors[$qidx][$pidx] = $msg; |
| 71 | + if ( $cidx === null ) { |
| 72 | + # proposal interpretation error message |
| 73 | + $this->qpcErrors[$qidx][$pidx] = $msg; |
| 74 | + return; |
| 75 | + } |
| 76 | + # proposal's category interpretation error message |
| 77 | + if ( !array_key_exists( $pidx, $this->qpcErrors[$qidx] ) || |
| 78 | + !is_array( $this->qpcErrors[$qidx][$pidx] ) ) { |
| 79 | + # remove previous proposal interpretation error message because |
| 80 | + # now we have more precise category interpretation error message |
| 81 | + $this->qpcErrors[$qidx][$pidx] = array(); |
| 82 | + } |
| 83 | + $this->qpcErrors[$qidx][$pidx][$cidx] = $msg; |
62 | 84 | } |
63 | 85 | |
64 | 86 | function setDefaultErrorMessage() { |
65 | | - if ( is_array( $this->qpErrors ) && $this->error == '' ) { |
| 87 | + if ( is_array( $this->qpcErrors ) && $this->error == '' ) { |
66 | 88 | $this->error = wfMsg( 'qp_interpetation_wrong_answer' ); |
67 | 89 | } |
68 | 90 | return $this; |
69 | 91 | } |
70 | 92 | |
71 | 93 | function isError() { |
72 | | - return $this->error != '' || is_array( $this->qpErrors ); |
| 94 | + return $this->error != '' || is_array( $this->qpcErrors ); |
73 | 95 | } |
74 | 96 | |
75 | 97 | } /* end of qp_InterpResult class */ |
— | — | @@ -865,10 +887,11 @@ |
866 | 888 | } |
867 | 889 | |
868 | 890 | private function setProposals() { |
| 891 | + global $wgContLang; |
869 | 892 | $insert = Array(); |
870 | 893 | foreach ( $this->Questions as $qkey => &$ques ) { |
871 | 894 | foreach ( $ques->ProposalText as $propkey => $ptext ) { |
872 | | - $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'proposal_id' => $propkey, 'proposal_text' => qp_Setup::limitProposalLength( $ptext ) ); |
| 895 | + $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'proposal_id' => $propkey, 'proposal_text' => $wgContLang->truncate( $ptext, qp_Setup::$proposal_max_length , '' ) ); |
873 | 896 | } |
874 | 897 | } |
875 | 898 | if ( count( $insert ) > 0 ) { |
Index: trunk/extensions/QPoll/view/qp_tabularquestionview.php |
— | — | @@ -244,6 +244,52 @@ |
245 | 245 | } |
246 | 246 | |
247 | 247 | /** |
| 248 | + * Render script-generated interpretation errors, when available (quiz mode) |
| 249 | + */ |
| 250 | + function renderInterpErrors() { |
| 251 | + if ( ( $interpErrors = $this->ctrl->getInterpErrors() ) === false ) { |
| 252 | + # there is no interpretation error |
| 253 | + return; |
| 254 | + } |
| 255 | + foreach ( $interpErrors as $prop_id => $prop_desc ) { |
| 256 | + if ( isset( $this->pview[$prop_id] ) ) { |
| 257 | + # the whole proposal line has errors |
| 258 | + $propview = &$this->pview[$prop_id]; |
| 259 | + if ( !is_array( $prop_desc ) ) { |
| 260 | + if ( !is_string( $prop_desc ) ) { |
| 261 | + $prop_desc = wfMsg( 'qp_interpetation_wrong_answer' ); |
| 262 | + } |
| 263 | + $propview->text = $this->bodyErrorMessage( $prop_desc, '', false ) . $propview->text; |
| 264 | + continue; |
| 265 | + } |
| 266 | + # specified category of proposal has errors; |
| 267 | + $foundCats = false; |
| 268 | + # scan the category views row to highlight erroneous categories |
| 269 | + foreach ( $propview->row as $cat_id => &$cat_tag ) { |
| 270 | + # only integer keys are the category views |
| 271 | + if ( is_int( $cat_id ) && isset( $prop_desc[$cat_id] ) ) { |
| 272 | + # found a category which has script-generated error |
| 273 | + $foundCats = true; |
| 274 | + # whether to use custom or standard error message |
| 275 | + if ( !is_string( $cat_desc = $prop_desc[$cat_id] ) ) { |
| 276 | + $cat_desc = wfMsg( 'qp_interpetation_wrong_answer' ); |
| 277 | + } |
| 278 | + # highlight the input |
| 279 | + qp_Renderer::addClass( $cat_tag, 'cat_error' ); |
| 280 | + array_unshift( $cat_tag, $this->bodyErrorMessage( $cat_desc, '', false ) . '<br />' ); |
| 281 | + } |
| 282 | + if ( !$foundCats ) { |
| 283 | + # there are category errors specified in interpretation result; |
| 284 | + # however none of them are found in proposal's view |
| 285 | + # generate error for the whole proposal |
| 286 | + $propview->text = $this->bodyErrorMessage( wfMsg( 'qp_interpetation_wrong_answer' ), '', false ) . $propview->text; |
| 287 | + } |
| 288 | + } |
| 289 | + } |
| 290 | + } |
| 291 | + } |
| 292 | + |
| 293 | + /** |
248 | 294 | * Draws borders via css in the question table according to |
249 | 295 | * controller's category spans (metacategories) |
250 | 296 | * todo: this function takes too much arguments; |
Index: trunk/extensions/QPoll/view/qp_pollstatsview.php |
— | — | @@ -42,16 +42,8 @@ |
43 | 43 | die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
44 | 44 | } |
45 | 45 | |
46 | | -class qp_PollStatsView extends qp_AbstractView { |
| 46 | +class qp_PollStatsView extends qp_AbstractPollView { |
47 | 47 | |
48 | | - var $perRow; |
49 | | - var $currCol; |
50 | | - |
51 | | - function setController( $ctrl, $perRow ) { |
52 | | - parent::setController( $ctrl ); |
53 | | - $this->perRow = $this->currCol = $perRow; |
54 | | - } |
55 | | - |
56 | 48 | function isCompatibleController( $ctrl ) { |
57 | 49 | return method_exists( $ctrl, 'parseStats' ); |
58 | 50 | } |
Index: trunk/extensions/QPoll/view/qp_stubquestionview.php |
— | — | @@ -136,31 +136,18 @@ |
137 | 137 | if ( $state != '' ) { |
138 | 138 | $this->ctrl->setState( $state, $msg ); |
139 | 139 | } |
140 | | - # return the message only for the first error occured |
141 | | - # (this one has to be short, because title attribute is being used) |
142 | 140 | if ( is_string( $rawClass ) ) { |
143 | 141 | $this->rawClass = $rawClass; |
144 | 142 | } |
145 | | - # show only the first error, when the state is not clean (not '') |
| 143 | + # will show only the first error, when the state is not clean (not '') |
146 | 144 | return ( $prev_state == '' ) ? '<span class="proposalerror" title="' . qp_Setup::specialchars( $msg ) . '">???</span> ' : ''; |
147 | 145 | } |
148 | 146 | |
149 | 147 | /** |
150 | | - * Render script-generated proposal errors, when available (quiz mode) |
151 | | - * Note: not being called in stats mode |
152 | | - * todo: implement separate category error hints in addition to the |
153 | | - * whole proposal row error hints (especially useful for text questions) |
| 148 | + * Render script-generated interpretation errors, when available (quiz mode) |
154 | 149 | */ |
155 | 150 | function renderInterpErrors() { |
156 | | - if ( ( $propErrors = $this->ctrl->getProposalsErrors() ) === false ) { |
157 | | - return; |
158 | | - } |
159 | | - foreach ( $this->pview as $prop_id => &$propview ) { |
160 | | - if ( isset( $propErrors[$prop_id] ) ) { |
161 | | - $msg = is_string( $propErrors[$prop_id] ) ? $propErrors[$prop_id] : wfMsg( 'qp_interpetation_wrong_answer' ); |
162 | | - $propview->text = $this->bodyErrorMessage( $msg, '', false ) . $propview->text; |
163 | | - } |
164 | | - } |
| 151 | + /* noop */ |
165 | 152 | } |
166 | 153 | |
167 | 154 | /** |
Index: trunk/extensions/QPoll/view/qp_pollview.php |
— | — | @@ -42,16 +42,8 @@ |
43 | 43 | die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
44 | 44 | } |
45 | 45 | |
46 | | -class qp_PollView extends qp_AbstractView { |
| 46 | +class qp_PollView extends qp_AbstractPollView { |
47 | 47 | |
48 | | - var $perRow; |
49 | | - var $currCol; |
50 | | - |
51 | | - function setController( $ctrl, $perRow ) { |
52 | | - parent::setController( $ctrl ); |
53 | | - $this->perRow = $this->currCol = $perRow; |
54 | | - } |
55 | | - |
56 | 48 | function isCompatibleController( $ctrl ) { |
57 | 49 | return method_exists( $ctrl, 'parsePoll' ); |
58 | 50 | } |
— | — | @@ -117,7 +109,7 @@ |
118 | 110 | # Determine the content of the settings table. |
119 | 111 | $settings = Array(); |
120 | 112 | if ( $this->ctrl->mState != '' ) { |
121 | | - $settings[0][] = array( '__tag' => 'td', 'class' => 'margin cell_error' ); |
| 113 | + $settings[0][] = array( '__tag' => 'td', 'class' => 'margin object_error' ); |
122 | 114 | $settings[0][] = array( '__tag' => 'td', 0 => wfMsgHtml( 'qp_result_' . $this->ctrl->mState ) ); |
123 | 115 | } |
124 | 116 | # Build the settings table. |
Index: trunk/extensions/QPoll/view/qp_abstractpollview.php |
— | — | @@ -0,0 +1,20 @@ |
| 2 | +<?php |
| 3 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 4 | + die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
| 5 | +} |
| 6 | + |
| 7 | +/** |
| 8 | + * Base class of poll views |
| 9 | + */ |
| 10 | +abstract class qp_AbstractPollView extends qp_AbstractView { |
| 11 | + |
| 12 | + # polls may have their questions displayed in a table |
| 13 | + # where $perRow specifies amount of questions in table row |
| 14 | + var $perRow; |
| 15 | + var $currCol; |
| 16 | + |
| 17 | + function setPerRow( $perRow ) { |
| 18 | + $this->perRow = $this->currCol = $perRow; |
| 19 | + } |
| 20 | + |
| 21 | +} /* end of qp_AbstractPollView class */ |
Property changes on: trunk/extensions/QPoll/view/qp_abstractpollview.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 22 | + native |
Index: trunk/extensions/QPoll/view/qp_textquestionview.php |
— | — | @@ -39,30 +39,24 @@ |
40 | 40 | |
41 | 41 | /** |
42 | 42 | * Manipulates the list of text question view tokens |
43 | | - * for one row (combined proposal/categories definition) |
| 43 | + * for one row (combined proposal/categories definition). |
| 44 | + * One proposal line. |
44 | 45 | */ |
45 | 46 | class qp_TextQuestionViewTokens { |
46 | 47 | |
47 | | - # an instance of current question's view |
48 | | - var $view; |
| 48 | + # $this->tklist will contain parsed tokens for question view; |
| 49 | + # elements of string type contain proposal parts; |
| 50 | + # elements of stdClass : |
| 51 | + # property 'options' indicates current category options list |
| 52 | + # property 'error' indicates error message |
| 53 | + var $tklist = array(); |
49 | 54 | |
50 | | - # $tokenslist will contain parsed tokens for question view; |
51 | | - # $tokenslist elements of string type contain proposal parts; |
52 | | - # $tokenslist elements of stdClass : |
53 | | - # property 'options' indicates current category options list |
54 | | - # property 'error' indicates error message |
55 | | - var $tokenslist = array(); |
56 | | - |
57 | | - function __construct( qp_TextQuestionView $view ) { |
58 | | - $this->view = $view; |
59 | | - } |
60 | | - |
61 | 55 | function reset() { |
62 | | - $this->tokenslist = array(); |
| 56 | + $this->tklist = array(); |
63 | 57 | } |
64 | 58 | |
65 | 59 | function addProposalPart( $prop ) { |
66 | | - $this->tokenslist[] = $prop; |
| 60 | + $this->tklist[] = $prop; |
67 | 61 | } |
68 | 62 | |
69 | 63 | /** |
— | — | @@ -97,23 +91,58 @@ |
98 | 92 | if ( !is_null( $opt->textwidth ) ) { |
99 | 93 | $catdef->textwidth = $opt->textwidth; |
100 | 94 | } |
101 | | - $this->tokenslist[] = $catdef; |
| 95 | + $this->tklist[] = $catdef; |
102 | 96 | } |
103 | 97 | |
104 | 98 | /** |
105 | 99 | * Adds new non-empty error message to the list of parsed tokens (viewtokens) |
106 | | - * @param $errmsg string error message |
| 100 | + * @param $errmsg string html error message |
107 | 101 | */ |
108 | | - function prependErrorToken( $errmsg, $state ) { |
| 102 | + function prependErrorToken( $errmsg ) { |
109 | 103 | # note: error message can be added in the middle of the list, |
110 | 104 | # for any category, if desired |
111 | | - # todo: separate category interpretation errors |
112 | | - if ( ( $html_msg = $this->view->bodyErrorMessage( $errmsg, $state ) ) !== '' ) { |
| 105 | + if ( $errmsg !== '' ) { |
113 | 106 | # usually only the first error message is returned |
114 | | - array_unshift( $this->tokenslist, (object) array( 'error'=> $html_msg ) ); |
| 107 | + array_unshift( $this->tklist, (object) array( 'error'=> $errmsg ) ); |
115 | 108 | } |
116 | 109 | } |
117 | 110 | |
| 111 | + /** |
| 112 | + * Applies interpretation script category error messages |
| 113 | + * to the current proposal line. |
| 114 | + * @param $prop_desc array |
| 115 | + * keys are category numbers (indexes) |
| 116 | + * values are interpretation script-generated error messages |
| 117 | + * @param $view an instance of current question view |
| 118 | + * @return boolean true when at least one category was found in the list |
| 119 | + * false otherwise |
| 120 | + */ |
| 121 | + function applyInterpErrors( $prop_desc, qp_TextQuestionView $view ) { |
| 122 | + $foundCats = false; |
| 123 | + $cat_id = -1; |
| 124 | + foreach ( $this->tklist as &$token ) { |
| 125 | + if ( is_object( $token ) && property_exists( $token, 'options' ) ) { |
| 126 | + # found a category definition |
| 127 | + $cat_id++; |
| 128 | + if ( isset( $prop_desc[$cat_id] ) ) { |
| 129 | + $foundCats = true; |
| 130 | + # whether to use custom or standard error message |
| 131 | + if ( !is_string( $cat_desc = $prop_desc[$cat_id] ) ) { |
| 132 | + $cat_desc = wfMsg( 'qp_interpetation_wrong_answer' ); |
| 133 | + } |
| 134 | + # mark the input to highlight it during the rendering |
| 135 | + if ( ( $msg = $view->bodyErrorMessage( $cat_desc, '', false ) ) !=='' ) { |
| 136 | + # we call with question state = '', so the returned $msg never should be empty |
| 137 | + # unless there was a syntax error, however during the interpretation stage there |
| 138 | + # should be no syntax errors, so we can assume that $msg is never equal to '' |
| 139 | + $token->interpError = $msg; |
| 140 | + } |
| 141 | + } |
| 142 | + } |
| 143 | + } |
| 144 | + return $foundCats; |
| 145 | + } |
| 146 | + |
118 | 147 | } /* end of qp_TextQuestionViewTokens */ |
119 | 148 | |
120 | 149 | /** |
— | — | @@ -153,7 +182,7 @@ |
154 | 183 | /** |
155 | 184 | * Add the list of parsed viewtokens matching current proposal / categories row |
156 | 185 | */ |
157 | | - function addProposal( $proposalId, $viewtokens ) { |
| 186 | + function addProposal( $proposalId, qp_TextQuestionViewTokens $viewtokens ) { |
158 | 187 | $this->pview[$proposalId] = array( |
159 | 188 | 'tokens' => $viewtokens, |
160 | 189 | 'className' => $this->rawClass |
— | — | @@ -161,6 +190,40 @@ |
162 | 191 | } |
163 | 192 | |
164 | 193 | /** |
| 194 | + * Render script-generated interpretation errors, when available (quiz mode) |
| 195 | + */ |
| 196 | + function renderInterpErrors() { |
| 197 | + if ( ( $interpErrors = $this->ctrl->getInterpErrors() ) === false ) { |
| 198 | + # there is no interpretation error |
| 199 | + return; |
| 200 | + } |
| 201 | + foreach ( $interpErrors as $prop_id => $prop_desc ) { |
| 202 | + if ( isset( $this->pview[$prop_id] ) ) { |
| 203 | + # the whole proposal line has errors |
| 204 | + $propview = &$this->pview[$prop_id]; |
| 205 | + if ( !is_array( $prop_desc ) ) { |
| 206 | + if ( !is_string( $prop_desc ) ) { |
| 207 | + $prop_desc = wfMsg( 'qp_interpetation_wrong_answer' ); |
| 208 | + } |
| 209 | + $propview['tokens']->prependErrorToken( $this->bodyErrorMessage( $prop_desc, '', false ) ); |
| 210 | + $propview['className'] = 'proposalerror'; |
| 211 | + continue; |
| 212 | + } |
| 213 | + # specified category of proposal has errors; |
| 214 | + # scan the category views row to highlight erroneous categories |
| 215 | + $foundCats = $propview['tokens']->applyInterpErrors( $prop_desc, $this ); |
| 216 | + if ( !$foundCats ) { |
| 217 | + # there are category errors specified in interpretation result; |
| 218 | + # however none of them are found in proposal's view |
| 219 | + # generate error for the whole proposal |
| 220 | + $propview['tokens']->prependErrorToken( $this->bodyErrorMessage( wfMsg( 'qp_interpetation_wrong_answer' ), '', false ) ); |
| 221 | + $propview['className'] = 'proposalerror'; |
| 222 | + } |
| 223 | + } |
| 224 | + } |
| 225 | + } |
| 226 | + |
| 227 | + /** |
165 | 228 | * |
166 | 229 | */ |
167 | 230 | function renderParsedProposal( &$viewtokens ) { |
— | — | @@ -172,6 +235,15 @@ |
173 | 236 | if ( $this->ctrl->mSubType === 'requireAllCategories' && $elem->unanswered ) { |
174 | 237 | $className = 'cat_noanswer'; |
175 | 238 | } |
| 239 | + if ( isset( $elem->interpError ) ) { |
| 240 | + $className = 'cat_noanswer'; |
| 241 | + # create view for proposal/category error message |
| 242 | + $row[] = array( |
| 243 | + '__tag' => 'span', |
| 244 | + 'class' => 'proposalerror', |
| 245 | + $elem->interpError |
| 246 | + ); |
| 247 | + } |
176 | 248 | # create view for the input options part |
177 | 249 | if ( count( $elem->options ) === 1 ) { |
178 | 250 | # one option produces html text input |
— | — | @@ -266,7 +338,7 @@ |
267 | 339 | qp_Renderer::addRow( $questionTable, $row, $rowattrs, 'th', $attribute_maps ); |
268 | 340 | } |
269 | 341 | foreach ( $this->pview as &$viewtokens ) { |
270 | | - $prop = $this->renderParsedProposal( $viewtokens['tokens'] ); |
| 342 | + $prop = $this->renderParsedProposal( $viewtokens['tokens']->tklist ); |
271 | 343 | $rowattrs = array( 'class' => $viewtokens['className'] ); |
272 | 344 | qp_Renderer::addRow( $questionTable, $prop, $rowattrs ); |
273 | 345 | } |
Index: trunk/extensions/QPoll/qp_eval.php |
— | — | @@ -388,7 +388,7 @@ |
389 | 389 | * @return array script result to check, or |
390 | 390 | * qp_InterpResult $interpResult (in case of error) |
391 | 391 | */ |
392 | | - function interpretAnswer( $interpretScript, $injectVars, qp_InterpResult $interpResult ) { |
| 392 | + static function interpretAnswer( $interpretScript, $injectVars, qp_InterpResult $interpResult ) { |
393 | 393 | # template page evaluation |
394 | 394 | if ( ( $check = self::selfCheck() ) !== true ) { |
395 | 395 | # self-check error |