Index: trunk/extensions/QPoll/i18n/qp.i18n.php |
— | — | @@ -126,6 +126,7 @@ |
127 | 127 | 'qp_error_too_long_category_options_values' => 'Category options values are too long to be stored in the database.', |
128 | 128 | 'qp_error_too_long_proposal_text' => 'Proposal text is too long to be stored in the database.', |
129 | 129 | 'qp_error_too_long_proposal_name' => 'Proposal name is too long to be stored in the database.', |
| 130 | + 'qp_error_invalid_proposal_name' => 'Proposal name cannot be numeric.', |
130 | 131 | 'qp_error_too_few_categories' => 'At least two categories must be defined.', |
131 | 132 | 'qp_error_too_few_spans' => 'Every category group must contain at least two subcategories.', |
132 | 133 | 'qp_error_no_answer' => 'Unanswered proposal.', |
— | — | @@ -243,6 +244,7 @@ |
244 | 245 | 'qp_error_too_long_category_options_values' => 'Question type="text" categories with more than one text option to chose are displayed as html select/options list. Submitted (chosen) options values are stored in the database field. If the total length of chosen values is too long, some of the values will be partially lost and select/options will not be properly highlighted. That\'s why the length limit is enforced.', |
245 | 246 | 'qp_error_too_long_proposal_text' => "Question type=\"text\" stores it's proposal parts and category definitions in 'proposal_text' field of database table, serialized. If serialized data is longer than database table field length, some of data will be lost and unserialization will be impossible.", |
246 | 247 | 'qp_error_too_long_proposal_name' => "Proposal name is defined to be used in interpretation scripts. It is stored in 'proposal_text' field of database table in such case. When the length of proposal name overflows the field length, the name will be truncated, and proposal will not be addressable by it's name in the interpretation script.", |
| 248 | + 'qp_error_invalid_proposal_name' => 'Proposal name should not be numeric to avoid possible reference clash with proposal ids, which are integer numbers.', |
247 | 249 | 'qp_error_too_few_spans' => 'Every category group should include at least two subcategories', |
248 | 250 | '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.', |
249 | 251 | 'qp_error_interpretation_no_return' => 'Interpretation script missed an return statement.', |
— | — | @@ -2783,6 +2785,7 @@ |
2784 | 2786 | 'qp_error_too_long_category_option_value' => 'Вариант ответа для данной категории слишком длинный для сохранения в базе данных', |
2785 | 2787 | 'qp_error_too_long_category_options_values' => 'Варианты ответов для данной категории слишком длинны для сохранения в базе данных', |
2786 | 2788 | 'qp_error_too_long_proposal_text' => 'Строка вопроса слишком длинна для сохранения в базе данных', |
| 2789 | + 'qp_error_invalid_proposal_name' => 'Имя строки вопроса не может быть числом.', |
2787 | 2790 | 'qp_error_too_few_categories' => 'Каждый вопрос должен иметь по крайней мере два варианта ответа', |
2788 | 2791 | 'qp_error_too_few_spans' => 'Каждая подкатегория вопроса требует по меньшей мере два варианта ответа', |
2789 | 2792 | 'qp_error_no_answer' => 'Нет ответа на вопрос', |
Index: trunk/extensions/QPoll/clientside/qp_user.js |
— | — | @@ -143,7 +143,7 @@ |
144 | 144 | |
145 | 145 | /** |
146 | 146 | * Makes this input to switch radiobuttons in the same row |
147 | | - * Used for questions type="mixed" |
| 147 | + * Used for question type="mixed" |
148 | 148 | */ |
149 | 149 | clickMixedRow : function() { |
150 | 150 | // example of input id: 'mx1q3p2c4' |
— | — | @@ -151,7 +151,7 @@ |
152 | 152 | }, |
153 | 153 | |
154 | 154 | /** |
155 | | - * Used for questions type="text", type="text!" |
| 155 | + * Used for question type="text" |
156 | 156 | */ |
157 | 157 | clickTextRow : function() { |
158 | 158 | // example of input id: 'tx1q3p2c4' |
— | — | @@ -196,7 +196,7 @@ |
197 | 197 | } |
198 | 198 | break; |
199 | 199 | case 'mx' : |
200 | | - // unset the row of checkboxes in case of "mixed" question type |
| 200 | + // unset the row of checkboxes in case of question type="mixed" |
201 | 201 | addEvent( input[j], "click", self.clickMixedRow ); |
202 | 202 | break; |
203 | 203 | case 'tx' : |
Index: trunk/extensions/QPoll/model/cache/qp_pollcache.php |
— | — | @@ -272,7 +272,7 @@ |
273 | 273 | } |
274 | 274 | |
275 | 275 | /** |
276 | | - * |
| 276 | + * Store row(s) both to DB and to memory cache. |
277 | 277 | */ |
278 | 278 | protected function storePolymorph() { |
279 | 279 | global $wgMemc; |
Index: trunk/extensions/QPoll/model/cache/qp_proposalcache.php |
— | — | @@ -28,9 +28,8 @@ |
29 | 29 | $pid = self::$store->pid; |
30 | 30 | foreach ( self::$store->Questions as $qkey => $qdata ) { |
31 | 31 | foreach ( $qdata->ProposalText as $propkey => $ptext ) { |
32 | | - if ( isset( $qdata->ProposalNames[$propkey] ) ) { |
33 | | - $ptext = qp_QuestionData::getProposalNamePrefix( $qdata->ProposalNames[$propkey] ) . $ptext; |
34 | | - } |
| 32 | + # note that $ptext already have proposal attributes packed and |
| 33 | + # already been checked for maximal length |
35 | 34 | $ptext = $wgContLang->truncate( $ptext, qp_Setup::$field_max_len['proposal_text'] , '' ); |
36 | 35 | $this->replace[] = array( 'pid' => $pid, 'question_id' => $qkey, 'proposal_id' => $propkey, 'proposal_text' => $ptext ); |
37 | 36 | # instead of calling $this->updateFromPollStore(), |
Index: trunk/extensions/QPoll/model/qp_questiondata.php |
— | — | @@ -31,7 +31,7 @@ |
32 | 32 | var $ProposalText; |
33 | 33 | # since v0.8.0a, proposals may be addressed by their names |
34 | 34 | # in the interpretation scripts |
35 | | - var $ProposalNames = array(); |
| 35 | + var $ProposalNames; |
36 | 36 | var $ProposalCategoryId; |
37 | 37 | var $ProposalCategoryText; |
38 | 38 | var $alreadyVoted = false; // whether the selected user already voted this question ? |
— | — | @@ -167,40 +167,6 @@ |
168 | 168 | } |
169 | 169 | } |
170 | 170 | |
171 | | - /** |
172 | | - * Split raw proposal text from source page text or from DB |
173 | | - * into name part / text part |
174 | | - * |
175 | | - * @param $proposal_text string raw proposal text |
176 | | - * @modifies $proposal_text string proposal text to display |
177 | | - * @return mixed |
178 | | - * string proposal name |
179 | | - * string '' when there is no name |
180 | | - * boolean false, when the name is too long thus cannot be stored in DB |
181 | | - */ |
182 | | - static function splitRawProposal( &$proposal_text ) { |
183 | | - $matches = array(); |
184 | | - $prop_name = ''; |
185 | | - preg_match( '`^:\|(.+?)\|\s*(.+?)$`u', $proposal_text, $matches ); |
186 | | - if ( count( $matches ) > 2 ) { |
187 | | - if ( ( $prop_name = trim( $matches[1] ) ) !== '' ) { |
188 | | - if ( strlen( $prop_name ) >= qp_Setup::$field_max_len['proposal_text'] ) { |
189 | | - return false; |
190 | | - } |
191 | | - # proposal name must be non-empty |
192 | | - $proposal_text = trim( $matches[2] ); |
193 | | - } |
194 | | - } |
195 | | - return $prop_name; |
196 | | - } |
197 | | - |
198 | | - /** |
199 | | - * Return proposal name prefix to be stored in DB (if any) |
200 | | - */ |
201 | | - static function getProposalNamePrefix( $name ) { |
202 | | - return ( $name !== '' ) ? ":|{$name}|" : ''; |
203 | | - } |
204 | | - |
205 | 171 | public function applyQuestion( qp_StubQuestion $question ) { |
206 | 172 | $this->question_id = $question->mQuestionId; |
207 | 173 | $this->type = $question->mType; |
Index: trunk/extensions/QPoll/model/qp_pollstore.php |
— | — | @@ -399,17 +399,17 @@ |
400 | 400 | $db = wfGetDB( DB_MASTER ); |
401 | 401 | $rows = qp_PollCache::load( $db, 'qp_ProposalCache' ); |
402 | 402 | # load proposal text from DB |
| 403 | + $prop_attrs = qp_Setup::$propAttrs; |
403 | 404 | foreach ( $rows as $row ) { |
404 | 405 | $question_id = $row->question_id; |
405 | 406 | $proposal_id = $row->proposal_id; |
406 | 407 | if ( $this->questionExists( $question_id ) ) { |
407 | 408 | $qdata = $this->Questions[ $question_id ]; |
408 | | - $prop_text = $row->proposal_text; |
409 | | - $prop_name = qp_QuestionData::splitRawProposal( $prop_text ); |
410 | | - if ( $prop_name !== false && $prop_name !== '' ) { |
411 | | - $qdata->ProposalNames[$proposal_id] = $prop_name; |
| 409 | + $prop_attrs->getFromDB( $row->proposal_text ); |
| 410 | + $qdata->ProposalText[$proposal_id] = $prop_attrs->dbText; |
| 411 | + if ( $prop_attrs->name !== '' ) { |
| 412 | + $qdata->ProposalNames[$proposal_id] = $prop_attrs->name; |
412 | 413 | } |
413 | | - $qdata->ProposalText[$proposal_id] = $prop_text; |
414 | 414 | } |
415 | 415 | } |
416 | 416 | } |
— | — | @@ -959,6 +959,10 @@ |
960 | 960 | $proposals[$catkey] = $qdata->ProposalCategoryText[ $propkey ][ $id_key ]; |
961 | 961 | } |
962 | 962 | } |
| 963 | + if ( count( $proposals ) === 0 ) { |
| 964 | + # 'catreq' = 0, pass one single empty cat to the interpretation script |
| 965 | + $proposals[0] = ''; |
| 966 | + } |
963 | 967 | if ( isset( $qdata->ProposalNames[$propkey] ) ) { |
964 | 968 | $questions[$qdata->ProposalNames[$propkey]] = $proposals; |
965 | 969 | } else { |
Index: trunk/extensions/QPoll/specials/qp_special.php |
— | — | @@ -48,6 +48,10 @@ |
49 | 49 | if ( self::$linker == null ) { |
50 | 50 | self::$linker = new Linker(); |
51 | 51 | } |
| 52 | + if ( qp_Setup::$propAttrs === null ) { |
| 53 | + qp_Setup::$propAttrs = new qp_PropAttrs(); |
| 54 | + } |
| 55 | + |
52 | 56 | parent::__construct( $name, $restriction, $listed, $function, $file, $includable ); |
53 | 57 | } |
54 | 58 | |
Index: trunk/extensions/QPoll/ctrl/qp_propattrs.php |
— | — | @@ -0,0 +1,142 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 5 | + die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
| 6 | +} |
| 7 | + |
| 8 | +/** |
| 9 | + * Get attributes from source (raw) proposal line or from DB field. |
| 10 | + * Build raw proposal line from existing attributes. |
| 11 | + */ |
| 12 | +class qp_PropAttrs { |
| 13 | + |
| 14 | + # code of error after getting attributes |
| 15 | + # 0 means there is no error |
| 16 | + public $error; |
| 17 | + # proposal name (for interpretation scripts); |
| 18 | + # '' means there is no name |
| 19 | + public $name; |
| 20 | + # count of required categories for question type="text"; |
| 21 | + # null means there is no 'catreq' attribute defined |
| 22 | + # (will use question/poll default) |
| 23 | + public $catreq; |
| 24 | + # qpoll tag source text of proposal: |
| 25 | + # * with source cat_parts / prop_parts |
| 26 | + # * without proposal attributes |
| 27 | + public $cpdef; |
| 28 | + # text of proposal prepared to be stored into DB |
| 29 | + # * without proposal attributes |
| 30 | + # * contains parsed cat_parts / prop_parts for question type="text" |
| 31 | + # * does not contain parsed cat_parts for another types of questions |
| 32 | + public $dbText; |
| 33 | + |
| 34 | + public function getFromDB( $proposal_text ) { |
| 35 | + $this->getFromSource( $proposal_text ); |
| 36 | + $this->dbText = $this->cpdef; |
| 37 | + $this->cpdef = null; |
| 38 | + # assume that DB state is always consistant |
| 39 | + $this->error = 0; |
| 40 | + } |
| 41 | + |
| 42 | + /** |
| 43 | + * Get proposal attributes from raw proposal text (source page text or DB field) |
| 44 | + * |
| 45 | + * @param $proposal_text string raw proposal text |
| 46 | + */ |
| 47 | + public function getFromSource( $proposal_text ) { |
| 48 | + # set default values of properties |
| 49 | + $this->error = 0; |
| 50 | + $this->name = ''; |
| 51 | + $this->dbText = |
| 52 | + $this->catreq = null; |
| 53 | + $this->cpdef = $proposal_text; |
| 54 | + $matches = array(); |
| 55 | + # try to match the raw proposal name (without specific attributes) |
| 56 | + preg_match( '`^:\|\s*(.+?)\s*\|\s*(.+?)\s*$`u', $this->cpdef, $matches ); |
| 57 | + if ( count( $matches ) < 3 || |
| 58 | + ( $this->name = $matches[1] ) === '' ) { |
| 59 | + # raw proposal name is not defined or empty |
| 60 | + return; |
| 61 | + } |
| 62 | + # check, whether raw proposal name will fit into the corresponding DB field |
| 63 | + if ( strlen( $this->getAttrDef() ) >= qp_Setup::$field_max_len['proposal_text'] ) { |
| 64 | + $this->setError( qp_Setup::ERROR_TOO_LONG_PROPNAME ); |
| 65 | + return; |
| 66 | + } |
| 67 | + # try to get xml-like attributes; |
| 68 | + $paramkeys = qp_Setup::getXmlLikeAttributes( $this->name, array( 'name', 'catreq' ) ); |
| 69 | + if ( $paramkeys['name'] !== null ) { |
| 70 | + # name attribute found |
| 71 | + $this->name = trim( $paramkeys['name'] ); |
| 72 | + } |
| 73 | + if ( $paramkeys['catreq'] !== null ) { |
| 74 | + $this->catreq = self::getSaneCatReq( $paramkeys['catreq'] ); |
| 75 | + } |
| 76 | + if ( is_numeric( $this->name ) ) { |
| 77 | + $this->setError( qp_Setup::ERROR_NUMERIC_PROPNAME ); |
| 78 | + return; |
| 79 | + } |
| 80 | + # remove raw proposal name from proposal definition |
| 81 | + $this->cpdef = $matches[2]; |
| 82 | + } |
| 83 | + |
| 84 | + /** |
| 85 | + * Get sanitized 'catreq' attribute value. |
| 86 | + */ |
| 87 | + public static function getSaneCatReq( $attr_val ) { |
| 88 | + $attr_val = trim( $attr_val ); |
| 89 | + if ( is_numeric( $attr_val ) ) { |
| 90 | + # return count of categories to be filled |
| 91 | + return ( $attr_val > 0 ) ? intval( $attr_val ) : 0; |
| 92 | + } |
| 93 | + # require all categories to be filled |
| 94 | + return 'all'; |
| 95 | + } |
| 96 | + |
| 97 | + /** |
| 98 | + * Set error state. |
| 99 | + * Make sure $this->cpdef contains the full raw proposal line, |
| 100 | + * otherwise the output of $this->__toString() will be incorrect. |
| 101 | + */ |
| 102 | + protected function setError( $code ) { |
| 103 | + $this->error = $code; |
| 104 | + $this->name = ''; |
| 105 | + $this->catreq = null; |
| 106 | + } |
| 107 | + |
| 108 | + /** |
| 109 | + * Return attributes part of raw proposal line. |
| 110 | + */ |
| 111 | + public function getAttrDef() { |
| 112 | + # we do not store 'catreq' attribute because: |
| 113 | + # 1. it's not used in qp_QuestionData |
| 114 | + # 2. we do not store poll's/question's catreq anyway |
| 115 | + return ( $this->name === '' ) ? '' : ":|{$this->name}|"; |
| 116 | + /* |
| 117 | + if ( $this->catreq === null ) { |
| 118 | + if ( $this->name === '' ) { |
| 119 | + return ''; |
| 120 | + } else { |
| 121 | + return ":|{$this->name}|"; |
| 122 | + } |
| 123 | + } else { |
| 124 | + if ( $this->name === '' ) { |
| 125 | + return ":|catreq=\"{$this->catreq}\"|"; |
| 126 | + } else { |
| 127 | + return ":|name=\"{$this->name}\" catreq=\"{$this->catreq}\"|"; |
| 128 | + } |
| 129 | + } |
| 130 | + */ |
| 131 | + } |
| 132 | + |
| 133 | + /** |
| 134 | + * Return raw proposal text to be stored in DB (if any) |
| 135 | + */ |
| 136 | + public function __toString() { |
| 137 | + if ( $this->dbText === null ) { |
| 138 | + throw new MWException( 'dbText is uninitialized in ' . __METHOD__ ); |
| 139 | + } |
| 140 | + return $this->getAttrDef() . $this->dbText; |
| 141 | + } |
| 142 | + |
| 143 | +} /* end of qp_PropAttrs class */ |
Property changes on: trunk/extensions/QPoll/ctrl/qp_propattrs.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 144 | + native |
Index: trunk/extensions/QPoll/ctrl/poll/qp_abstractpoll.php |
— | — | @@ -78,7 +78,7 @@ |
79 | 79 | * possible xml-like attributes the question may have |
80 | 80 | */ |
81 | 81 | var $questionAttributeKeys = array( |
82 | | - 't[yi]p[eo]', 'name', 'layout', 'textwidth', 'propwidth', 'showresults' |
| 82 | + 't[yi]p[eo]', 'name', 'catreq', 'layout', 'textwidth', 'propwidth', 'showresults' |
83 | 83 | ); |
84 | 84 | |
85 | 85 | /** |
— | — | @@ -89,9 +89,10 @@ |
90 | 90 | * that can be partially merged from poll to question (similar to CSS) |
91 | 91 | */ |
92 | 92 | var $defaultQuestionAttributes = array( |
93 | | - 'propwidth' => null, |
| 93 | + 'catreq' => null, |
94 | 94 | 'layout' => null, |
95 | | - 'textwidth' => null |
| 95 | + 'textwidth' => null, |
| 96 | + 'propwidth' => null |
96 | 97 | ); |
97 | 98 | |
98 | 99 | /** |
— | — | @@ -181,7 +182,7 @@ |
182 | 183 | # quote the params (if any) |
183 | 184 | $args = array_map( array( 'qp_Setup', 'specialchars' ), $args ); |
184 | 185 | array_unshift( $args, $key ); |
185 | | - return call_user_func_array( array( self, 'fatalErrorNoQuote' ), $args ); |
| 186 | + return call_user_func_array( array( __CLASS__, 'fatalErrorNoQuote' ), $args ); |
186 | 187 | } |
187 | 188 | |
188 | 189 | static function s_getPollTitleFragment( $pollid, $dash = '#' ) { |
Index: trunk/extensions/QPoll/ctrl/question/qp_abstractquestion.php |
— | — | @@ -15,19 +15,19 @@ |
16 | 16 | # when the collection of the questions is not sparce (was not randomized) |
17 | 17 | var $mQuestionId; |
18 | 18 | |
19 | | - var $mState = ''; // current state of question parsing (no error) |
| 19 | + # current state of question parsing (no error) |
| 20 | + var $mState = ''; |
20 | 21 | # default type of the question; stored in DB; |
21 | 22 | # should always be properly initialized in parent controller via $poll->parseMainHeader() |
22 | 23 | var $mType = 'unknown'; |
23 | | - # some questions has a subtype; currently is not stored in DB; |
24 | | - # should always be properly initialized in parent controller via $poll->parseMainHeader() |
25 | | - var $mSubType = ''; |
26 | 24 | var $mCategories = array(); |
27 | 25 | var $mCategorySpans = array(); |
28 | | - var $mCommonQuestion = ''; // common question of this question |
29 | | - var $mProposalNames = array(); // an array of question proposals names (optional, used in interpretation scripts) |
30 | | - var $mProposalText = array(); // an array of question proposals |
31 | | - var $alreadyVoted = false; // whether the selected user has already voted this question ? |
| 26 | + # common question of this question |
| 27 | + var $mCommonQuestion = ''; |
| 28 | + # an array of question proposals |
| 29 | + var $mProposalText = array(); |
| 30 | + # whether the selected user has already voted this question? |
| 31 | + var $alreadyVoted = false; |
32 | 32 | |
33 | 33 | # statistics |
34 | 34 | var $Percents = null; |
— | — | @@ -94,10 +94,6 @@ |
95 | 95 | $this->view->setPropWidth( $paramkeys[ 'propwidth' ] ); |
96 | 96 | } |
97 | 97 | |
98 | | - function getProposalIdByName( $proposalName ) { |
99 | | - return array_search( $proposalName, $this->mProposalNames, true ); |
100 | | - } |
101 | | - |
102 | 98 | function getPercents( $proposalId, $catId ) { |
103 | 99 | if ( is_array( $this->Percents ) && array_key_exists( $proposalId, $this->Percents ) && |
104 | 100 | is_array( $this->Percents[ $proposalId ] ) && array_key_exists( $catId, $this->Percents[ $proposalId ] ) ) { |
Index: trunk/extensions/QPoll/ctrl/question/qp_textquestion.php |
— | — | @@ -194,6 +194,8 @@ |
195 | 195 | */ |
196 | 196 | class qp_TextQuestion extends qp_StubQuestion { |
197 | 197 | |
| 198 | + # required count of single proposal categories that should be filled by user |
| 199 | + var $mCatReq = 'all'; |
198 | 200 | # regexp for separation of proposal line tokens |
199 | 201 | var $propCatPattern; |
200 | 202 | |
— | — | @@ -257,13 +259,18 @@ |
258 | 260 | */ |
259 | 261 | function applyAttributes( array $paramkeys ) { |
260 | 262 | parent::applyAttributes( $paramkeys ); |
261 | | - if ( $this->mSubType === 'requireAllCategories' ) { |
| 263 | + # commented out, because now the 'catreq' attribute is set per proposal row, thus |
| 264 | + # it is unpractical to disable radiobuttons for all proposals of the question. |
| 265 | + # todo: disable radiobuttons per proposal, when current catreq=0 ? |
| 266 | + /* |
| 267 | + if ( $this->mCatReq === 'all' ) { |
262 | 268 | # radio button prevents from filling all categories, disable it |
263 | 269 | if ( ( $radio_brace = array_search( 'radio', $this->input_braces_types, true ) ) !== false ) { |
264 | 270 | unset( $this->input_braces_types[$radio_brace] ); |
265 | 271 | unset( $this->matching_braces[$radio_brace] ); |
266 | 272 | } |
267 | 273 | } |
| 274 | + */ |
268 | 275 | $braces_list = array_map( 'preg_quote', |
269 | 276 | array_merge( |
270 | 277 | ( array_values( $this->matching_braces ) ), |
— | — | @@ -461,21 +468,27 @@ |
462 | 469 | $opt = new qp_TextQuestionOptions(); |
463 | 470 | # set static view state for the future qp_TextQuestionProposalView instances |
464 | 471 | qp_TextQuestionProposalView::applyViewState( $this->view ); |
| 472 | + $prop_attrs = qp_Setup::$propAttrs; |
465 | 473 | foreach ( $this->raws as $raw ) { |
466 | 474 | $opt->reset(); |
467 | 475 | $this->propview = new qp_TextQuestionProposalView( $proposalId, $this ); |
468 | | - # set proposal name (if any) |
469 | | - if ( ( $prop_name = qp_QuestionData::splitRawProposal( $raw ) ) === false ) { |
470 | | - # we do not need to generate error for too long proposal name, |
471 | | - # because the length limit will be enforced on the whole serialized |
472 | | - # proposal string (with proposal_name + cat_parts + prop_parts) |
473 | | - $prop_name = ''; |
| 476 | + # get proposal name and optional attributes (if any) |
| 477 | + $prop_attrs->getFromSource( $raw ); |
| 478 | + if ( $prop_attrs->error === qp_Setup::ERROR_TOO_LONG_PROPNAME ) { |
| 479 | + $this->propview->prependErrorToken( wfMsg( 'qp_error_too_long_proposal_name' ), 'error' ); |
| 480 | + } elseif ( $prop_attrs->error === qp_Setup::ERROR_NUMERIC_PROPNAME ) { |
| 481 | + $this->propview->prependErrorToken( wfMsg( 'qp_error_invalid_proposal_name' ), 'error' ); |
474 | 482 | } |
475 | 483 | $this->dbtokens = $brace_stack = array(); |
476 | 484 | $dbtokens_idx = -1; |
477 | 485 | $catId = 0; |
478 | 486 | $last_brace = ''; |
479 | | - $this->rawtokens = preg_split( $this->propCatPattern, $raw, -1, PREG_SPLIT_DELIM_CAPTURE ); |
| 487 | + $this->rawtokens = preg_split( |
| 488 | + $this->propCatPattern, |
| 489 | + $prop_attrs->cpdef, |
| 490 | + -1, |
| 491 | + PREG_SPLIT_DELIM_CAPTURE |
| 492 | + ); |
480 | 493 | $matching_closed_brace = ''; |
481 | 494 | $this->findMatchingBraces(); |
482 | 495 | $this->backtrackMismatchingBraces(); |
— | — | @@ -540,41 +553,31 @@ |
541 | 554 | } |
542 | 555 | # check if there is at least one category defined |
543 | 556 | if ( $catId === 0 ) { |
544 | | - # todo: this is the explanary line, it is not real proposal |
| 557 | + # todo: this is the explanatory line, it is not real proposal |
545 | 558 | $this->propview->prependErrorToken( wfMsg( 'qp_error_too_few_categories' ), 'error' ); |
546 | 559 | } |
547 | | - $proposal_text = serialize( $this->dbtokens ); |
| 560 | + $prop_attrs->dbText = serialize( $this->dbtokens ); |
548 | 561 | # build the whole raw DB proposal_text value to check it's maximal length |
549 | | - if ( strlen( qp_QuestionData::getProposalNamePrefix( $prop_name ) . $proposal_text ) > qp_Setup::$field_max_len['proposal_text'] ) { |
| 562 | + if ( strlen( $prop_attrs ) > qp_Setup::$field_max_len['proposal_text'] ) { |
550 | 563 | # too long proposal field to store into the DB |
551 | 564 | # this is very important check for text questions because |
552 | 565 | # category definitions are stored within the proposal text |
553 | 566 | $this->propview->prependErrorToken( wfMsg( 'qp_error_too_long_proposal_text' ), 'error' ); |
554 | 567 | } |
555 | | - $this->mProposalText[$proposalId] = $proposal_text; |
556 | | - if ( $prop_name !== '' ) { |
557 | | - $this->mProposalNames[$proposalId] = $prop_name; |
| 568 | + $this->mProposalText[$proposalId] = strval( $prop_attrs ); |
| 569 | + if ( $prop_attrs->name !== '' ) { |
| 570 | + $this->mProposalNames[$proposalId] = $prop_attrs->name; |
558 | 571 | } |
559 | | - if ( $this->poll->mBeingCorrected ) { |
560 | | - # check for unanswered categories |
561 | | - try { |
562 | | - if ( !array_key_exists( $proposalId, $this->mProposalCategoryId ) ) { |
563 | | - # the whole line is unanswered |
564 | | - throw new Exception( 'qp_error' ); |
565 | | - } |
566 | | - # how many categories has to be answered, |
567 | | - # all defined in row or at least one? |
568 | | - $countRequired = ($this->mSubType === 'requireAllCategories') ? $catId : 1; |
569 | | - if ( count( $this->mProposalCategoryId[$proposalId] ) < $countRequired ) { |
570 | | - throw new Exception( 'qp_error' ); |
571 | | - } |
572 | | - } catch ( Exception $e ) { |
573 | | - if ( $e->getMessage() == 'qp_error' ) { |
574 | | - $prev_state = $this->getState(); |
575 | | - $this->propview->prependErrorToken( wfMsg( 'qp_error_no_answer' ), 'NA' ); |
576 | | - } |
577 | | - } |
| 572 | + if ( ( $catreq = $prop_attrs->catreq ) === null ) { |
| 573 | + $catreq = $this->mCatReq; |
578 | 574 | } |
| 575 | + $this->propview->catreq = $catreq; |
| 576 | + ## Check for unanswered categories. |
| 577 | + if ( $this->poll->mBeingCorrected && |
| 578 | + $this->hasMissingCategories( $proposalId, $catreq, $catId ) ) { |
| 579 | + $prev_state = $this->getState(); |
| 580 | + $this->propview->prependErrorToken( wfMsg( 'qp_error_no_answer' ), 'NA' ); |
| 581 | + } |
579 | 582 | $this->view->addProposal( $proposalId, $this->propview ); |
580 | 583 | $proposalId++; |
581 | 584 | } |
Index: trunk/extensions/QPoll/ctrl/question/qp_mixedquestion.php |
— | — | @@ -10,6 +10,9 @@ |
11 | 11 | */ |
12 | 12 | class qp_MixedQuestion extends qp_TabularQuestion { |
13 | 13 | |
| 14 | + # required count of single proposal categories that should be filled by user |
| 15 | + var $mCatReq = 1; |
| 16 | + |
14 | 17 | /** |
15 | 18 | * Creates question view which should be renreded and |
16 | 19 | * also may be altered during the poll generation |
— | — | @@ -24,55 +27,59 @@ |
25 | 28 | $proposalId = -1; |
26 | 29 | # set static view state for the future qp_TabularQuestionProposalView instances |
27 | 30 | qp_TabularQuestionProposalView::applyViewState( $this->view ); |
| 31 | + $prop_attrs = qp_Setup::$propAttrs; |
28 | 32 | foreach ( $this->raws as $raw ) { |
| 33 | + # get proposal attributes |
| 34 | + $prop_attrs->getFromSource( $raw ); |
29 | 35 | # new proposal view |
30 | 36 | $pview = new qp_TabularQuestionProposalView( $proposalId + 1, $this ); |
31 | | - if ( preg_match( $this->mProposalPattern, $raw, $matches ) ) { |
32 | | - $pview->text = array_pop( $matches ); // current proposal text |
| 37 | + # get the list of categories ($matches) |
| 38 | + if ( preg_match( $this->mProposalPattern, $prop_attrs->cpdef, $matches ) ) { |
| 39 | + $prop_attrs->dbText = array_pop( $matches ); // current proposal text |
33 | 40 | array_shift( $matches ); // remove "at whole" match |
34 | 41 | $last_matches = $matches; |
35 | 42 | } else { |
36 | 43 | if ( $proposalId >= 0 ) { |
37 | 44 | # shortened syntax: use the pattern from the last row where it's been defined |
38 | | - $pview->text = $raw; |
| 45 | + $prop_attrs->dbText = $prop_attrs->cpdef; |
39 | 46 | $matches = $last_matches; |
40 | 47 | } |
41 | 48 | } |
42 | | - if ( $pview->text === null ) { |
| 49 | + if ( $prop_attrs->dbText === null ) { |
43 | 50 | continue; |
44 | 51 | } |
| 52 | + $pview->text = $prop_attrs->dbText; |
45 | 53 | $proposalId++; |
46 | 54 | # set proposal name (if any) |
47 | | - $prop_name = qp_QuestionData::splitRawProposal( $pview->text ); |
48 | | - if ( $prop_name === false ) { |
| 55 | + if ( $prop_attrs->error === qp_Setup::ERROR_TOO_LONG_PROPNAME ) { |
49 | 56 | $pview->prependErrorMessage( wfMsg( 'qp_error_too_long_proposal_name' ), 'error' ); |
50 | | - } elseif ( $prop_name !== '' ) { |
51 | | - $this->mProposalNames[$proposalId] = $prop_name; |
| 57 | + } elseif ( $prop_attrs->error === qp_Setup::ERROR_NUMERIC_PROPNAME ) { |
| 58 | + $pview->prependErrorMessage( wfMsg( 'qp_error_invalid_proposal_name' ), 'error' ); |
| 59 | + } elseif ( $prop_attrs->name !== '' ) { |
| 60 | + $this->mProposalNames[$proposalId] = $prop_attrs->name; |
52 | 61 | } |
53 | | - $this->mProposalText[$proposalId] = trim( $pview->text ); |
| 62 | + $this->mProposalText[$proposalId] = strval( $prop_attrs ); |
54 | 63 | # Determine a type ID, according to the questionType and the number of signes. |
55 | 64 | foreach ( $this->mCategories as $catId => $catDesc ) { |
56 | | - $typeId = $matches[ $catId ]; |
| 65 | + $typeId = $matches[$catId]; |
57 | 66 | # start new input field tag (category) |
58 | 67 | $pview->addNewCategory( $catId ); |
59 | 68 | $inp = array( '__tag' => 'input' ); |
60 | 69 | # Determine the input's name and value. |
| 70 | + $name = "q{$this->mQuestionId}p{$proposalId}s{$catId}"; |
61 | 71 | switch ( $typeId ) { |
62 | | - case '<>': |
63 | | - $name = 'q' . $this->mQuestionId . 'p' . $proposalId . 's' . $catId; |
64 | | - $value = ''; |
65 | | - $inputType = 'text'; |
66 | | - break; |
67 | | - case '[]': |
68 | | - $name = 'q' . $this->mQuestionId . 'p' . $proposalId . 's' . $catId; |
69 | | - $value = 's' . $catId; |
70 | | - $inputType = 'checkbox'; |
71 | | - break; |
72 | | - case '()': |
73 | | - $name = 'q' . $this->mQuestionId . 'p' . $proposalId . 's' . $catId; |
74 | | - $value = 's' . $catId; |
75 | | - $inputType = 'radio'; |
76 | | - break; |
| 72 | + case '<>': |
| 73 | + $value = ''; |
| 74 | + $inputType = 'text'; |
| 75 | + break; |
| 76 | + case '[]': |
| 77 | + $value = "s{$catId}"; |
| 78 | + $inputType = 'checkbox'; |
| 79 | + break; |
| 80 | + case '()': |
| 81 | + $value = "s{$catId}"; |
| 82 | + $inputType = 'radio'; |
| 83 | + break; |
77 | 84 | } |
78 | 85 | # Determine if the input has to be checked. |
79 | 86 | $input_checked = false; |
— | — | @@ -133,12 +140,16 @@ |
134 | 141 | $pview->setErrorMessage( wfMsg( 'qp_error_proposal_text_empty' ), 'error' ); |
135 | 142 | throw new Exception( 'qp_error' ); |
136 | 143 | } |
137 | | - # If the proposal was submitted but unanswered |
138 | | - if ( $this->poll->mBeingCorrected && !array_key_exists( $proposalId, $this->mProposalCategoryId ) ) { |
139 | | - $prev_state = $this->getState(); |
| 144 | + ## Check for unanswered categories. |
| 145 | + if ( ( $catreq = $prop_attrs->catreq ) === null ) { |
| 146 | + $catreq = $this->mCatReq; |
| 147 | + } |
| 148 | + if ( $this->poll->mBeingCorrected && |
| 149 | + $this->hasMissingCategories( $proposalId, $catreq, count( $this->mCategories ) ) ) { |
| 150 | + # the proposal was submitted but has not enough answered categories |
140 | 151 | $pview->prependErrorMessage( wfMsg( 'qp_error_no_answer' ), 'NA' ); |
141 | 152 | # if there was no previous errors, hightlight the whole row |
142 | | - if ( $prev_state == '' ) { |
| 153 | + if ( $this->getState() == '' ) { |
143 | 154 | throw new Exception( 'qp_error' ); |
144 | 155 | } |
145 | 156 | } |
Index: trunk/extensions/QPoll/ctrl/question/qp_tabularquestion.php |
— | — | @@ -279,22 +279,25 @@ |
280 | 280 | $proposalId = -1; |
281 | 281 | # set static view state for the future qp_TabularQuestionProposalView instances |
282 | 282 | qp_TabularQuestionProposalView::applyViewState( $this->view ); |
| 283 | + $prop_attrs = qp_Setup::$propAttrs; |
283 | 284 | foreach ( $this->raws as $raw ) { |
284 | | - if ( !preg_match( $this->mProposalPattern, $raw, $matches ) ) { |
| 285 | + # get proposal attributes |
| 286 | + $prop_attrs->getFromSource( $raw ); |
| 287 | + if ( !preg_match( $this->mProposalPattern, $prop_attrs->cpdef, $matches ) ) { |
285 | 288 | continue; |
286 | 289 | } |
287 | 290 | # new proposal view |
288 | 291 | $pview = new qp_TabularQuestionProposalView( $proposalId + 1, $this ); |
289 | 292 | $proposalId++; |
290 | | - $pview->text = array_pop( $matches ); |
291 | | - # set proposal name (if any) |
292 | | - $prop_name = qp_QuestionData::splitRawProposal( $pview->text ); |
293 | | - if ( $prop_name === false ) { |
| 293 | + $prop_attrs->dbText = $pview->text = array_pop( $matches ); |
| 294 | + if ( $prop_attrs->error === qp_Setup::ERROR_TOO_LONG_PROPNAME ) { |
294 | 295 | $pview->prependErrorMessage( wfMsg( 'qp_error_too_long_proposal_name' ), 'error' ); |
295 | | - } elseif ( $prop_name !== '' ) { |
296 | | - $this->mProposalNames[$proposalId] = $prop_name; |
| 296 | + } elseif ( $prop_attrs->error === qp_Setup::ERROR_NUMERIC_PROPNAME ) { |
| 297 | + $pview->prependErrorMessage( wfMsg( 'qp_error_invalid_proposal_name' ), 'error' ); |
| 298 | + } elseif ( $prop_attrs->name !== '' ) { |
| 299 | + $this->mProposalNames[$proposalId] = $prop_attrs->name; |
297 | 300 | } |
298 | | - $this->mProposalText[$proposalId] = trim( $pview->text ); |
| 301 | + $this->mProposalText[$proposalId] = strval( $prop_attrs ); |
299 | 302 | foreach ( $this->mCategories as $catId => $catDesc ) { |
300 | 303 | # start new input field tag (category) |
301 | 304 | $pview->addNewCategory( $catId ); |
— | — | @@ -303,12 +306,12 @@ |
304 | 307 | # Determine the input's name and value. |
305 | 308 | switch( $this->mType ) { |
306 | 309 | case 'multipleChoice': |
307 | | - $name = 'q' . $this->mQuestionId . 'p' . $proposalId . 's' . $catId; |
308 | | - $value = 's' . $catId; |
| 310 | + $name = "q{$this->mQuestionId}p{$proposalId}s{$catId}"; |
| 311 | + $value = "s{$catId}"; |
309 | 312 | break; |
310 | 313 | case 'singleChoice': |
311 | | - $name = 'q' . $this->mQuestionId . 'p' . $proposalId; |
312 | | - $value = 's' . $catId; |
| 314 | + $name = "q{$this->mQuestionId}p{$proposalId}"; |
| 315 | + $value = "s{$catId}"; |
313 | 316 | # category spans have sense only with single choice proposals |
314 | 317 | $pview->renderSpan( $name, $value, $catDesc ); |
315 | 318 | break; |
Index: trunk/extensions/QPoll/ctrl/question/qp_stubquestion.php |
— | — | @@ -15,6 +15,14 @@ |
16 | 16 | # optional question literal name, used to address questions in interpretation scripts |
17 | 17 | var $mName = null; |
18 | 18 | |
| 19 | + # some questions have subtype; currently is not stored in DB; |
| 20 | + # should always be properly initialized in parent controller via $poll->parseMainHeader() |
| 21 | + var $mSubType = ''; |
| 22 | + |
| 23 | + # array of question proposals names (optional, used in interpretation scripts) |
| 24 | + # packed to string together with mProposalText then stored into DB field 'proposal_text' |
| 25 | + var $mProposalNames = array(); |
| 26 | + |
19 | 27 | # current user voting taken from POST data (if available) |
20 | 28 | var $mProposalCategoryId = Array(); // user true/false answers to the question's proposal |
21 | 29 | var $mProposalCategoryText = Array(); // user text answers to the question's proposal |
— | — | @@ -40,10 +48,60 @@ |
41 | 49 | $this->mName = $name; |
42 | 50 | } |
43 | 51 | |
| 52 | + /** |
| 53 | + * Get question key (reference) |
| 54 | + * @return mixed |
| 55 | + * string question name if available, otherwise |
| 56 | + * integer question id |
| 57 | + */ |
44 | 58 | function getQuestionKey() { |
45 | 59 | return $this->mName === null ? $this->mQuestionId : $this->mName; |
46 | 60 | } |
47 | 61 | |
| 62 | + /** |
| 63 | + * Get proposal id by proposal name, if any. |
| 64 | + * @param $proposalName string |
| 65 | + * proposal name |
| 66 | + * @return mixed |
| 67 | + * integer question id for specified name |
| 68 | + * false there is no such name |
| 69 | + */ |
| 70 | + function getProposalIdByName( $proposalName ) { |
| 71 | + return array_search( $proposalName, $this->mProposalNames, true ); |
| 72 | + } |
| 73 | + |
| 74 | + /** |
| 75 | + * Checks, whether current proposal has not enough of user-answered categories, |
| 76 | + * according to current qp_Setup::$propAttrs. |
| 77 | + * @param $proposalId integer |
| 78 | + * id of existing question's proposal |
| 79 | + * @param $catreq mixed |
| 80 | + * value of catreq attribute |
| 81 | + * string 'all' |
| 82 | + * integer count of required categories |
| 83 | + * @param $prop_cats_count |
| 84 | + * integer total amount of categories in current proposal |
| 85 | + * @return boolean |
| 86 | + * true not enough of categories are filled |
| 87 | + * false otherwise |
| 88 | + */ |
| 89 | + function hasMissingCategories( $proposal_id, $catreq, $prop_cats_count ) { |
| 90 | + # How many categories has to be answered, |
| 91 | + # all defined in row or the amount specified by 'catreq' attribute? |
| 92 | + # total amount of categories in current proposal |
| 93 | + $prop_cats_count = count( $this->mCategories ); |
| 94 | + $countRequired = ($catreq === 'all') ? $prop_cats_count : $catreq; |
| 95 | + if ( $countRequired > $prop_cats_count ) { |
| 96 | + # do not require to fill more categories |
| 97 | + # than is available in current proposal row |
| 98 | + $countRequired = $prop_cats_count; |
| 99 | + } |
| 100 | + $answered_cat_count = array_key_exists( $proposal_id, $this->mProposalCategoryId ) ? |
| 101 | + count( $this->mProposalCategoryId[$proposal_id] ) : |
| 102 | + 0; |
| 103 | + return $answered_cat_count < $countRequired; |
| 104 | + } |
| 105 | + |
48 | 106 | # load some question fields from qp_QuestionData given |
49 | 107 | # (usually qp_QuestionData is an array property of qp_PollStore instance) |
50 | 108 | # @param $qdata - an instance of qp_QuestionData |
— | — | @@ -109,6 +167,19 @@ |
110 | 168 | } |
111 | 169 | |
112 | 170 | /** |
| 171 | + * Applies previousely parsed attributes from main header into question's view |
| 172 | + * (all attributes but type) |
| 173 | + * @param $paramkeys array |
| 174 | + * key is attribute name regexp match, value is the value of attribute |
| 175 | + */ |
| 176 | + function applyAttributes( array $paramkeys ) { |
| 177 | + parent::applyAttributes( $paramkeys ); |
| 178 | + if ( $paramkeys['catreq'] !== null ) { |
| 179 | + $this->mCatReq = qp_PropAttrs::getSaneCatReq( $paramkeys['catreq'] ); |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + /** |
113 | 184 | * @return mixed |
114 | 185 | * array (associative) of script-generated interpretation error messages |
115 | 186 | * for current question proposals (and optionally categories); |
Index: trunk/extensions/QPoll/qp_user.php |
— | — | @@ -142,8 +142,11 @@ |
143 | 143 | class qp_Setup { |
144 | 144 | |
145 | 145 | # internal unique error codes |
| 146 | + const NO_ERROR = 0; |
146 | 147 | const ERROR_MISSED_TITLE = 1; |
147 | 148 | const ERROR_INVALID_ADDRESS = 2; |
| 149 | + const ERROR_TOO_LONG_PROPNAME = 3; |
| 150 | + const ERROR_NUMERIC_PROPNAME = 4; |
148 | 151 | |
149 | 152 | # unicode entity used to display selected checkboxes and radiobuttons in |
150 | 153 | # result views at Special:Pollresults page |
— | — | @@ -154,7 +157,7 @@ |
155 | 158 | # matches string which contains integer number in range 1..9999 |
156 | 159 | const PREG_POSITIVE_INT4_MATCH = '/^[1-9]\d{0,3}$/'; |
157 | 160 | |
158 | | - ## separators of lines / values for question type 'text' / 'text!' |
| 161 | + ## separators of lines / values for question type="text" |
159 | 162 | # these should not be the same and should not appear in valid text; |
160 | 163 | # characters that are used to separate values of select multiple |
161 | 164 | const SELECT_MULTIPLE_VALUES_SEPARATOR = "\r"; |
— | — | @@ -177,6 +180,9 @@ |
178 | 181 | static $user; // User instance recieved from hook |
179 | 182 | static $request; // WebRequest instance recieved from hook |
180 | 183 | |
| 184 | + # single instance for extracting attributes from proposal lines |
| 185 | + static $propAttrs = null; |
| 186 | + |
181 | 187 | /** |
182 | 188 | * The map of question 'type' attribute value to the question's ctrl / view / subtype. |
183 | 189 | */ |
— | — | @@ -205,12 +211,6 @@ |
206 | 212 | 'text' => array( |
207 | 213 | 'ctrl' => 'qp_TextQuestion', |
208 | 214 | 'view' => 'qp_TextQuestionView', |
209 | | - 'mType' => 'textQuestion', |
210 | | - 'mSubType' => 'requireAllCategories' |
211 | | - ), |
212 | | - 'text!' => array( |
213 | | - 'ctrl' => 'qp_TextQuestion', |
214 | | - 'view' => 'qp_TextQuestionView', |
215 | 215 | 'mType' => 'textQuestion' |
216 | 216 | ) |
217 | 217 | ); |
— | — | @@ -364,6 +364,8 @@ |
365 | 365 | 'ctrl/question/qp_mixedquestion.php' => 'qp_MixedQuestion', |
366 | 366 | 'ctrl/question/qp_textquestion.php' => array( 'qp_TextQuestionOptions', 'qp_TextQuestion' ), |
367 | 367 | 'ctrl/question/qp_questionstats.php' => 'qp_QuestionStats', |
| 368 | + # proposal attributes |
| 369 | + 'ctrl/qp_propattrs.php' => 'qp_PropAttrs', |
368 | 370 | # interpretation results |
369 | 371 | 'ctrl/qp_interpresult.php' => 'qp_InterpResult', |
370 | 372 | |
— | — | @@ -464,6 +466,7 @@ |
465 | 467 | $wgGroupPermissions['bureaucrat']['editinterpretation'] = true; |
466 | 468 | |
467 | 469 | $wgDebugLogGroups['qpoll'] = 'qpoll_debug_log.txt'; |
| 470 | + |
468 | 471 | } |
469 | 472 | |
470 | 473 | static function mediaWikiVersionCompare( $version, $operator = '<' ) { |
— | — | @@ -615,6 +618,7 @@ |
616 | 619 | } elseif ( $request->getVal( 'pollId' ) !== null ) { |
617 | 620 | self::clearCache(); |
618 | 621 | } |
| 622 | + self::$propAttrs = new qp_PropAttrs(); |
619 | 623 | return true; |
620 | 624 | } |
621 | 625 | |
Index: trunk/extensions/QPoll/view/proposal/qp_textquestionproposalview.php |
— | — | @@ -10,6 +10,8 @@ |
11 | 11 | */ |
12 | 12 | class qp_TextQuestionProposalView extends qp_StubQuestionProposalView { |
13 | 13 | |
| 14 | + # count of required categories to be non-empty in current proposal |
| 15 | + var $catreq = 'all'; |
14 | 16 | # list of viewtokens |
15 | 17 | # elements of string type contain proposal parts; |
16 | 18 | # elements of stdClass : |
Index: trunk/extensions/QPoll/view/question/qp_textquestionview.php |
— | — | @@ -343,23 +343,24 @@ |
344 | 344 | } |
345 | 345 | |
346 | 346 | /** |
347 | | - * Generates tagarray representation from the list of viewtokens. |
348 | | - * @param $pkey proposal index (starting from 0): |
349 | | - * it is required for JS code, because text questions |
350 | | - * now may optionally have "tabular transposed" layout. |
351 | | - * @param $viewtokens array of viewtokens |
| 347 | + * Generates tagarray representation from the proposal view. |
| 348 | + * @param $pkey integer |
| 349 | + * proposal index (starting from 0): |
| 350 | + * it is required for JS code, because text questions |
| 351 | + * now may optionally have "tabular transposed" layout. |
| 352 | + * @param $propview qp_TextQuestionProposalView |
352 | 353 | * @return tagarray |
353 | 354 | */ |
354 | | - function renderParsedProposal( $pkey, array &$viewtokens ) { |
| 355 | + function renderParsedProposal( $pkey, qp_TextQuestionProposalView $propview ) { |
355 | 356 | $vr = $this->vr; |
356 | 357 | # proposal prefix for category tag id generation |
357 | 358 | $vr->reset( "tx{$this->ctrl->poll->mOrderId}q{$this->ctrl->mQuestionId}p{$pkey}" ); |
358 | | - foreach ( $viewtokens as $elem ) { |
| 359 | + foreach ( $propview->viewtokens as $elem ) { |
359 | 360 | $vr->cell = array(); |
360 | 361 | if ( is_object( $elem ) ) { |
361 | 362 | if ( isset( $elem->options ) ) { |
362 | 363 | $className = 'cat_part'; |
363 | | - if ( $this->ctrl->mSubType === 'requireAllCategories' && $elem->unanswered ) { |
| 364 | + if ( $propview->catreq === 'all' && $elem->unanswered ) { |
364 | 365 | $className .= ' cat_noanswer'; |
365 | 366 | } |
366 | 367 | if ( isset( $elem->interpError ) ) { |
— | — | @@ -423,7 +424,7 @@ |
424 | 425 | qp_Renderer::addRow( $questionTable, $row, $rowattrs, 'th', $attribute_maps ); |
425 | 426 | } |
426 | 427 | foreach ( $this->pviews as $pkey => $propview ) { |
427 | | - $prop = $this->renderParsedProposal( $pkey, $propview->viewtokens ); |
| 428 | + $prop = $this->renderParsedProposal( $pkey, $propview ); |
428 | 429 | $rowattrs = array( 'class' => $propview->rowClass ); |
429 | 430 | if ( $this->transposed ) { |
430 | 431 | qp_Renderer::addColumn( $questionTable, $prop, $rowattrs ); |