Index: trunk/extensions/QPoll/clientside/qp_user.css |
— | — | @@ -43,7 +43,7 @@ |
44 | 44 | .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; } |
45 | 45 | .qpoll .script_view { font-family: monospace, "Courier New"; white-space:pre; overflow:auto; color:black; background-color: lightgray; border-right: 1px solid darkgray; border-top: 1px solid darkgray; border-bottom: 1px solid darkgray; padding: 0.5em; } |
46 | 46 | |
47 | | -/* LTR part (RTL is included from separate file */ |
| 47 | +/* LTR part (RTL is included from separate file) */ |
48 | 48 | .qpoll .attempts_counter{ border: 1px solid gray; padding: 0.1em 0.5em 0.1em 0.5em; color: black; background-color: lightblue; margin-left: 1em; } |
49 | 49 | .qpoll .question { margin-left:2em; } |
50 | 50 | .qpoll .margin { padding-left:20px; } |
Index: trunk/extensions/QPoll/ctrl/qp_abstractpoll.php |
— | — | @@ -67,13 +67,26 @@ |
68 | 68 | |
69 | 69 | # current state of poll parsing (no error) |
70 | 70 | var $mState = ''; |
71 | | - # // true, when the poll is posted (answered) |
| 71 | + # true, when the poll is posted (answered) |
72 | 72 | var $mBeingCorrected = false; |
73 | 73 | |
74 | | - // qp_pollStore instance that will be used to transfer poll data from/to DB |
| 74 | + # qp_pollStore instance that will be used to transfer poll data from/to DB |
75 | 75 | var $pollStore = null; |
76 | 76 | |
77 | 77 | /** |
| 78 | + * default values of 'propwidth', 'textwidth' and 'layout' attributes |
| 79 | + * will be applied to child questions that do not have these attributes defined |
| 80 | + * |
| 81 | + * 'showresults' currently is handled separately, because it has "multivalue" |
| 82 | + * and can be partially merged from poll to question |
| 83 | + */ |
| 84 | + var $defaultQuestionAttributes = array( |
| 85 | + 'propwidth' => null, |
| 86 | + 'layout' => null, |
| 87 | + 'textwidth' => null |
| 88 | + ); |
| 89 | + |
| 90 | + /** |
78 | 91 | * Constructor |
79 | 92 | * |
80 | 93 | * @public |
— | — | @@ -102,7 +115,12 @@ |
103 | 116 | } |
104 | 117 | $this->view->showResults = self::parseShowResults( $argv['showresults'] ); |
105 | 118 | } |
106 | | - |
| 119 | + # get default question attributes, if any |
| 120 | + foreach ( $this->defaultQuestionAttributes as $attr => &$val ) { |
| 121 | + if ( array_key_exists( $attr, $argv ) ) { |
| 122 | + $val = $argv[$attr]; |
| 123 | + } |
| 124 | + } |
107 | 125 | # every poll on the page should have unique poll id, to minimize the risk of collisions |
108 | 126 | # it is required to be set manually via id="value" parameter |
109 | 127 | # ( used only in "declaration" mode ) |
— | — | @@ -203,18 +221,23 @@ |
204 | 222 | |
205 | 223 | /** |
206 | 224 | * Parses attribute line of the question |
207 | | - * @param $attr_str attribute string from poll's header |
| 225 | + * @param $attr_str attribute string from questions header |
208 | 226 | * @modifies $paramkeys array key is attribute regexp, value is the value of attribute |
209 | 227 | * @return string the value of question's type attribute |
210 | 228 | */ |
211 | 229 | function getQuestionAttributes( $attr_str, &$paramkeys ) { |
212 | | - $paramkeys = array( 't[yi]p[eo]' => null, 'layout' => null, 'textwidth' => null, 'showresults' => null ); |
| 230 | + $paramkeys = array( 't[yi]p[eo]' => null, 'layout' => null, 'textwidth' => null, 'propwidth' => null, 'showresults' => null ); |
213 | 231 | foreach ( $paramkeys as $key => &$val ) { |
214 | 232 | preg_match( '`' . $key . '?="(.*?)"`u', $attr_str, $val ); |
| 233 | + $val = ( count( $val ) > 1 ) ? $val[1] : null; |
215 | 234 | } |
216 | | - $type = $paramkeys[ 't[yi]p[eo]' ]; |
217 | | - $type = isset( $type[1] ) ? trim( $type[1] ) : ''; |
218 | | - return $type; |
| 235 | + # apply default question attributes, if any |
| 236 | + foreach ( $this->defaultQuestionAttributes as $attr => $val ) { |
| 237 | + if ( is_null( $paramkeys[$attr] ) ) { |
| 238 | + $paramkeys[$attr] = $val; |
| 239 | + } |
| 240 | + } |
| 241 | + return isset( $paramkeys[ 't[yi]p[eo]' ] ) ? trim( $paramkeys[ 't[yi]p[eo]' ] ) : ''; |
219 | 242 | } |
220 | 243 | |
221 | 244 | // parses source showresults xml parameter value and returns the corresponding showResults array |
Index: trunk/extensions/QPoll/ctrl/qp_abstractquestion.php |
— | — | @@ -89,6 +89,7 @@ |
90 | 90 | function applyAttributes( $paramkeys ) { |
91 | 91 | $this->view->setLayout( $paramkeys[ 'layout' ], $paramkeys[ 'textwidth' ] ); |
92 | 92 | $this->view->setShowResults( $paramkeys[ 'showresults' ] ); |
| 93 | + $this->view->setPropWidth( $paramkeys[ 'propwidth' ] ); |
93 | 94 | } |
94 | 95 | |
95 | 96 | function getPercents( $proposalId, $catId ) { |
Index: trunk/extensions/QPoll/ctrl/qp_textquestion.php |
— | — | @@ -65,7 +65,7 @@ |
66 | 66 | * @param $token string current value of token between pipe separators |
67 | 67 | * Also, _optionally_ overrides textwidth property |
68 | 68 | */ |
69 | | - function setLastOption( $token ) { |
| 69 | + function addToLastOption( $token ) { |
70 | 70 | # first entry of category options might be definition of |
71 | 71 | # the current category input textwidth instead |
72 | 72 | $matches = array(); |
— | — | @@ -76,7 +76,7 @@ |
77 | 77 | $this->textwidth = intval( $matches[1] ); |
78 | 78 | } else { |
79 | 79 | # add new input option |
80 | | - $this->iopt_last .= trim( $token ); |
| 80 | + $this->iopt_last .= $token; |
81 | 81 | } |
82 | 82 | } |
83 | 83 | |
— | — | @@ -86,7 +86,7 @@ |
87 | 87 | function closeCategory() { |
88 | 88 | $this->isCatDef = false; |
89 | 89 | # prepare new category input choice (text questions have no category names) |
90 | | - $unique_options = array_unique( $this->input_options, SORT_STRING ); |
| 90 | + $unique_options = array_unique( array_map( 'trim', $this->input_options ), SORT_STRING ); |
91 | 91 | $this->input_options = array(); |
92 | 92 | foreach ( $unique_options as $option ) { |
93 | 93 | # make sure unique elements keys are consequitive starting from 0 |
— | — | @@ -189,8 +189,12 @@ |
190 | 190 | switch ( $token ) { |
191 | 191 | case '|' : |
192 | 192 | if ( $opt->isCatDef ) { |
193 | | - $opt->addEmptyOption(); |
194 | | - $isContinue = true; |
| 193 | + if ( count( $brace_stack ) == 1 && $brace_stack[0] === '>>' ) { |
| 194 | + # pipe char starts new option only at top brace level, |
| 195 | + # with angled braces |
| 196 | + $opt->addEmptyOption(); |
| 197 | + $isContinue = true; |
| 198 | + } |
195 | 199 | } |
196 | 200 | break; |
197 | 201 | case '[[' : |
— | — | @@ -230,7 +234,7 @@ |
231 | 235 | continue; |
232 | 236 | } |
233 | 237 | if ( $opt->isCatDef ) { |
234 | | - $opt->setLastOption( $token ); |
| 238 | + $opt->addToLastOption( $token ); |
235 | 239 | } else { |
236 | 240 | # add new proposal part |
237 | 241 | $this->dbtokens[] = strval( $token ); |
— | — | @@ -249,16 +253,24 @@ |
250 | 254 | $this->viewtokens->prependErrorToken( wfMsg( 'qp_error_too_long_proposal_text' ), 'error' ); |
251 | 255 | } |
252 | 256 | $this->mProposalText[$proposalId] = $proposal_text; |
253 | | - # If the proposal was submitted but has _any_ unanswered category |
254 | | - if ( $this->poll->mBeingCorrected && |
255 | | - ( !array_key_exists( $proposalId, $this->mProposalCategoryId ) || |
256 | | - count( $this->mProposalCategoryId[$proposalId] ) !== $catId ) |
257 | | - ) { |
258 | | - # todo: apply 'error' style to the whole row |
259 | | - $prev_state = $this->getState(); |
260 | | - $this->viewtokens->prependErrorToken( wfMsg( 'qp_error_no_answer' ), 'NA' ); |
261 | | - if ( $prev_state == '' ) { |
262 | | - # todo: if there was no previous errors, hightlight the whole row |
| 257 | + if ( $this->poll->mBeingCorrected ) { |
| 258 | + # check for unanswered categories |
| 259 | + try { |
| 260 | + if ( !array_key_exists( $proposalId, $this->mProposalCategoryId ) ) { |
| 261 | + # the whole line is unanswered |
| 262 | + throw new Exception( 'qp_error' ); |
| 263 | + } |
| 264 | + # how many categories has to be answered, |
| 265 | + # all defined in row or at least one? |
| 266 | + $countRequired = ($this->mSubType === 'requireAllCategories') ? $catId : 1; |
| 267 | + if ( count( $this->mProposalCategoryId[$proposalId] ) < $countRequired ) { |
| 268 | + throw new Exception( 'qp_error' ); |
| 269 | + } |
| 270 | + } catch ( Exception $e ) { |
| 271 | + if ( $e->getMessage() == 'qp_error' ) { |
| 272 | + $prev_state = $this->getState(); |
| 273 | + $this->viewtokens->prependErrorToken( wfMsg( 'qp_error_no_answer' ), 'NA' ); |
| 274 | + } |
263 | 275 | } |
264 | 276 | } |
265 | 277 | $this->view->addProposal( $proposalId, $this->viewtokens->tokenslist ); |
Index: trunk/extensions/QPoll/ctrl/qp_tabularquestion.php |
— | — | @@ -107,6 +107,9 @@ |
108 | 108 | # analyze previousely built "raw" categories array |
109 | 109 | # Less than two categories is a syntax error. |
110 | 110 | if ( $this->mType != 'mixedChoice' && count( $categories ) < 2 ) { |
| 111 | + if ( !isset( $categories[0] ) ) { |
| 112 | + $categories[0] = ''; |
| 113 | + } |
111 | 114 | $categories[0] .= $this->view->bodyErrorMessage( wfMsg( 'qp_error_too_few_categories' ), 'error' ); |
112 | 115 | } |
113 | 116 | foreach ( $categories as $catkey => $category ) { |
Index: trunk/extensions/QPoll/qp_user.php |
— | — | @@ -138,30 +138,36 @@ |
139 | 139 | static $user; // User instance we got from hook parameter |
140 | 140 | |
141 | 141 | static $questionTypes = array( |
142 | | - 'mixed' => array( |
143 | | - 'ctrl' => 'qp_MixedQuestion', |
144 | | - 'view' => 'qp_TabularQuestionView', |
145 | | - 'mType' => 'mixedChoice' |
146 | | - ), |
147 | | - 'unique()' => array( |
| 142 | + '[]' => array( |
148 | 143 | 'ctrl' => 'qp_TabularQuestion', |
149 | 144 | 'view' => 'qp_TabularQuestionView', |
150 | | - 'mType' => 'singleChoice', |
151 | | - 'mSubType' => 'unique' |
| 145 | + 'mType' => 'multipleChoice' |
152 | 146 | ), |
153 | 147 | '()' => array( |
154 | 148 | 'ctrl' => 'qp_TabularQuestion', |
155 | 149 | 'view' => 'qp_TabularQuestionView', |
156 | 150 | 'mType' => 'singleChoice' |
157 | 151 | ), |
158 | | - '[]' => array( |
| 152 | + 'unique()' => array( |
159 | 153 | 'ctrl' => 'qp_TabularQuestion', |
160 | 154 | 'view' => 'qp_TabularQuestionView', |
161 | | - 'mType' => 'multipleChoice' |
| 155 | + 'mType' => 'singleChoice', |
| 156 | + 'mSubType' => 'unique' |
162 | 157 | ), |
| 158 | + 'mixed' => array( |
| 159 | + 'ctrl' => 'qp_MixedQuestion', |
| 160 | + 'view' => 'qp_TabularQuestionView', |
| 161 | + 'mType' => 'mixedChoice' |
| 162 | + ), |
163 | 163 | 'text' => array( |
164 | 164 | 'ctrl' => 'qp_TextQuestion', |
165 | 165 | 'view' => 'qp_TextQuestionView', |
| 166 | + 'mType' => 'textQuestion', |
| 167 | + 'mSubType' => 'requireAllCategories' |
| 168 | + ), |
| 169 | + 'text!' => array( |
| 170 | + 'ctrl' => 'qp_TextQuestion', |
| 171 | + 'view' => 'qp_TextQuestionView', |
166 | 172 | 'mType' => 'textQuestion' |
167 | 173 | ) |
168 | 174 | ); |
— | — | @@ -207,6 +213,33 @@ |
208 | 214 | } |
209 | 215 | |
210 | 216 | /** |
| 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 | + /** |
211 | 244 | * Autoload classes from the map provided |
212 | 245 | */ |
213 | 246 | static function autoLoad( $map ) { |
Index: trunk/extensions/QPoll/qp_pollstore.php |
— | — | @@ -867,8 +867,8 @@ |
868 | 868 | private function setProposals() { |
869 | 869 | $insert = Array(); |
870 | 870 | foreach ( $this->Questions as $qkey => &$ques ) { |
871 | | - foreach ( $ques->ProposalText as $propkey => &$ptext ) { |
872 | | - $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'proposal_id' => $propkey, 'proposal_text' => $ptext ); |
| 871 | + foreach ( $ques->ProposalText as $propkey => $ptext ) { |
| 872 | + $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'proposal_id' => $propkey, 'proposal_text' => qp_Setup::limitProposalLength( $ptext ) ); |
873 | 873 | } |
874 | 874 | } |
875 | 875 | if ( count( $insert ) > 0 ) { |
Index: trunk/extensions/QPoll/view/qp_tabularquestionview.php |
— | — | @@ -92,9 +92,9 @@ |
93 | 93 | } |
94 | 94 | |
95 | 95 | function setLayout( $layout, $textwidth ) { |
96 | | - if ( count( $layout ) > 0 ) { |
97 | | - $this->transposed = strpos( $layout[1], 'transpose' ) !== false; |
98 | | - $this->proposalsFirst = strpos( $layout[1], 'proposals' ) !== false; |
| 96 | + if ( $layout !== null ) { |
| 97 | + $this->transposed = strpos( $layout, 'transpose' ) !== false; |
| 98 | + $this->proposalsFirst = strpos( $layout, 'proposals' ) !== false; |
99 | 99 | } |
100 | 100 | # setup question layout parameters |
101 | 101 | if ( $this->transposed ) { |
— | — | @@ -110,8 +110,8 @@ |
111 | 111 | $this->proposalTextStyle = 'vertical-align:middle; '; |
112 | 112 | $this->proposalTextStyle .= ( $this->proposalsFirst ) ? 'padding-right: 10px;' : 'padding-left: 10px;'; |
113 | 113 | } |
114 | | - if ( count( $textwidth ) > 0 ) { |
115 | | - $textwidth = intval( $textwidth[1] ); |
| 114 | + if ( $textwidth !== null ) { |
| 115 | + $textwidth = intval( $textwidth ); |
116 | 116 | if ( $textwidth > 0 ) { |
117 | 117 | $this->textInputStyle = 'width:' . $textwidth . 'em;'; |
118 | 118 | } |
— | — | @@ -120,9 +120,9 @@ |
121 | 121 | |
122 | 122 | function setShowResults( $showresults ) { |
123 | 123 | # setup question's showresults when global showresults != 0 |
124 | | - if ( qp_Setup::$global_showresults != 0 && count( $showresults ) > 0 ) { |
| 124 | + if ( qp_Setup::$global_showresults != 0 && $showresults !== null ) { |
125 | 125 | # use the value from the question |
126 | | - $this->showResults = qp_AbstractPoll::parseShowResults( $showresults[1] ); |
| 126 | + $this->showResults = qp_AbstractPoll::parseShowResults( $showresults ); |
127 | 127 | # apply undefined attributes from the poll's showresults definition |
128 | 128 | foreach ( $this->pollShowResults as $attr => $val ) { |
129 | 129 | if ( $attr != 'type' && !isset( $this->showResults[$attr] ) ) { |
— | — | @@ -143,6 +143,24 @@ |
144 | 144 | } |
145 | 145 | |
146 | 146 | /** |
| 147 | + * Checks, whether the supplied CSS length value is valid |
| 148 | + * @return boolean true for valid value, false otherwise |
| 149 | + */ |
| 150 | + function isCSSLengthValid( $width ) { |
| 151 | + preg_match( '`^\s*(\d+)(px|em|en|%|)\s*$`', $width, $matches ); |
| 152 | + return count( $matches > 1 ) && $matches[1] > 0; |
| 153 | + } |
| 154 | + |
| 155 | + function setPropWidth( $attr ) { |
| 156 | + if ( $attr !== null && $this->isCSSLengthValid( $attr ) ) { |
| 157 | + $this->propWidth = trim( $attr ); |
| 158 | + } |
| 159 | + if ( $this->propWidth !== '' ) { |
| 160 | + $this->proposalTextStyle .= " width:{$this->propWidth};"; |
| 161 | + } |
| 162 | + } |
| 163 | + |
| 164 | + /** |
147 | 165 | * Builds tagarray of categories |
148 | 166 | * @param $categories "raw" categories |
149 | 167 | */ |
Index: trunk/extensions/QPoll/view/qp_stubquestionview.php |
— | — | @@ -58,6 +58,8 @@ |
59 | 59 | # proposal views (indexed, sortable rows) |
60 | 60 | var $pview = array(); |
61 | 61 | |
| 62 | + var $propWidth = ''; |
| 63 | + |
62 | 64 | /** |
63 | 65 | * @param $parser |
64 | 66 | * @param $frame |
— | — | @@ -90,6 +92,10 @@ |
91 | 93 | /* does nothing */ |
92 | 94 | } |
93 | 95 | |
| 96 | + function setPropWidth( $propwidth ) { |
| 97 | + /* does nothing */ |
| 98 | + } |
| 99 | + |
94 | 100 | /** |
95 | 101 | * @param $tagarray array / string row to add to the question's header |
96 | 102 | */ |
— | — | @@ -186,6 +192,9 @@ |
187 | 193 | */ |
188 | 194 | function renderQuestion() { |
189 | 195 | $output_table = array( '__tag' => 'table', '__end' => "\n", 'class' => 'object' ); |
| 196 | + if ( $this->propWidth !== '' ) { |
| 197 | + $output_table['style'] = 'width:100%;'; |
| 198 | + } |
190 | 199 | # Determine the side border color the question. |
191 | 200 | if ( $this->ctrl->getState() != '' ) { |
192 | 201 | if ( isset( $output_table['class'] ) ) { |
— | — | @@ -206,7 +215,7 @@ |
207 | 216 | ); |
208 | 217 | } |
209 | 218 | $tags[] = array( '__tag' => 'div', 0 => $this->rtp( $this->ctrl->mCommonQuestion ) ); |
210 | | - $tags = array( '__tag' => 'div', '__end' => "\n", 'class' => 'question', $tags ); |
| 219 | + $tags = array( '__tag' => 'div', '__end' => "\n", 'class' => 'question question_mod4_' . ( $this->ctrl->usedId % 4 ), $tags ); |
211 | 220 | $tags[] = &$output_table; |
212 | 221 | return qp_Renderer::renderTagArray( $tags ); |
213 | 222 | } |
Index: trunk/extensions/QPoll/view/qp_textquestionview.php |
— | — | @@ -142,8 +142,8 @@ |
143 | 143 | |
144 | 144 | function setLayout( $layout, $textwidth ) { |
145 | 145 | /* todo: implement vertical layout */ |
146 | | - if ( count( $textwidth ) > 0 ) { |
147 | | - $textwidth = intval( $textwidth[1] ); |
| 146 | + if ( $textwidth !== null ) { |
| 147 | + $textwidth = intval( $textwidth ); |
148 | 148 | if ( $textwidth > 0 ) { |
149 | 149 | $this->textInputStyle = 'width:' . $textwidth . 'em;'; |
150 | 150 | } |
— | — | @@ -168,7 +168,10 @@ |
169 | 169 | foreach ( $viewtokens as $elem ) { |
170 | 170 | if ( is_object( $elem ) ) { |
171 | 171 | if ( isset( $elem->options ) ) { |
172 | | - $className = $elem->unanswered ? 'cat_noanswer' : 'cat_part'; |
| 172 | + $className = 'cat_part'; |
| 173 | + if ( $this->ctrl->mSubType === 'requireAllCategories' && $elem->unanswered ) { |
| 174 | + $className = 'cat_noanswer'; |
| 175 | + } |
173 | 176 | # create view for the input options part |
174 | 177 | if ( count( $elem->options ) === 1 ) { |
175 | 178 | # one option produces html text input |