r104025 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r104024‎ | r104025 | r104026 >
Date:11:42, 23 November 2011
Author:questpc
Status:deferred
Tags:
Comment:
Implemented proposal attributes. Currently only two optional attributes are available: "name", which defines proposal name for interpretation scripts and "catreq", which specifies how many categories have to be filled by user in current proposal. "catreq" attribute is also inherited from poll / question (when undefined in proposal).
Modified paths:
  • /trunk/extensions/QPoll/clientside/qp_user.js (modified) (history)
  • /trunk/extensions/QPoll/ctrl/poll/qp_abstractpoll.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/qp_propattrs.php (added) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_abstractquestion.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_mixedquestion.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_stubquestion.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_tabularquestion.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_textquestion.php (modified) (history)
  • /trunk/extensions/QPoll/i18n/qp.i18n.php (modified) (history)
  • /trunk/extensions/QPoll/model/cache/qp_pollcache.php (modified) (history)
  • /trunk/extensions/QPoll/model/cache/qp_proposalcache.php (modified) (history)
  • /trunk/extensions/QPoll/model/qp_pollstore.php (modified) (history)
  • /trunk/extensions/QPoll/model/qp_questiondata.php (modified) (history)
  • /trunk/extensions/QPoll/qp_user.php (modified) (history)
  • /trunk/extensions/QPoll/specials/qp_special.php (modified) (history)
  • /trunk/extensions/QPoll/view/proposal/qp_textquestionproposalview.php (modified) (history)
  • /trunk/extensions/QPoll/view/question/qp_textquestionview.php (modified) (history)

Diff [purge]

Index: trunk/extensions/QPoll/i18n/qp.i18n.php
@@ -126,6 +126,7 @@
127127 'qp_error_too_long_category_options_values' => 'Category options values are too long to be stored in the database.',
128128 'qp_error_too_long_proposal_text' => 'Proposal text is too long to be stored in the database.',
129129 '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.',
130131 'qp_error_too_few_categories' => 'At least two categories must be defined.',
131132 'qp_error_too_few_spans' => 'Every category group must contain at least two subcategories.',
132133 'qp_error_no_answer' => 'Unanswered proposal.',
@@ -243,6 +244,7 @@
244245 '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.',
245246 '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.",
246247 '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.',
247249 'qp_error_too_few_spans' => 'Every category group should include at least two subcategories',
248250 '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.',
249251 'qp_error_interpretation_no_return' => 'Interpretation script missed an return statement.',
@@ -2783,6 +2785,7 @@
27842786 'qp_error_too_long_category_option_value' => 'Вариант ответа для данной категории слишком длинный для сохранения в базе данных',
27852787 'qp_error_too_long_category_options_values' => 'Варианты ответов для данной категории слишком длинны для сохранения в базе данных',
27862788 'qp_error_too_long_proposal_text' => 'Строка вопроса слишком длинна для сохранения в базе данных',
 2789+ 'qp_error_invalid_proposal_name' => 'Имя строки вопроса не может быть числом.',
27872790 'qp_error_too_few_categories' => 'Каждый вопрос должен иметь по крайней мере два варианта ответа',
27882791 'qp_error_too_few_spans' => 'Каждая подкатегория вопроса требует по меньшей мере два варианта ответа',
27892792 'qp_error_no_answer' => 'Нет ответа на вопрос',
Index: trunk/extensions/QPoll/clientside/qp_user.js
@@ -143,7 +143,7 @@
144144
145145 /**
146146 * Makes this input to switch radiobuttons in the same row
147 - * Used for questions type="mixed"
 147+ * Used for question type="mixed"
148148 */
149149 clickMixedRow : function() {
150150 // example of input id: 'mx1q3p2c4'
@@ -151,7 +151,7 @@
152152 },
153153
154154 /**
155 - * Used for questions type="text", type="text!"
 155+ * Used for question type="text"
156156 */
157157 clickTextRow : function() {
158158 // example of input id: 'tx1q3p2c4'
@@ -196,7 +196,7 @@
197197 }
198198 break;
199199 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"
201201 addEvent( input[j], "click", self.clickMixedRow );
202202 break;
203203 case 'tx' :
Index: trunk/extensions/QPoll/model/cache/qp_pollcache.php
@@ -272,7 +272,7 @@
273273 }
274274
275275 /**
276 - *
 276+ * Store row(s) both to DB and to memory cache.
277277 */
278278 protected function storePolymorph() {
279279 global $wgMemc;
Index: trunk/extensions/QPoll/model/cache/qp_proposalcache.php
@@ -28,9 +28,8 @@
2929 $pid = self::$store->pid;
3030 foreach ( self::$store->Questions as $qkey => $qdata ) {
3131 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
3534 $ptext = $wgContLang->truncate( $ptext, qp_Setup::$field_max_len['proposal_text'] , '' );
3635 $this->replace[] = array( 'pid' => $pid, 'question_id' => $qkey, 'proposal_id' => $propkey, 'proposal_text' => $ptext );
3736 # instead of calling $this->updateFromPollStore(),
Index: trunk/extensions/QPoll/model/qp_questiondata.php
@@ -31,7 +31,7 @@
3232 var $ProposalText;
3333 # since v0.8.0a, proposals may be addressed by their names
3434 # in the interpretation scripts
35 - var $ProposalNames = array();
 35+ var $ProposalNames;
3636 var $ProposalCategoryId;
3737 var $ProposalCategoryText;
3838 var $alreadyVoted = false; // whether the selected user already voted this question ?
@@ -167,40 +167,6 @@
168168 }
169169 }
170170
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 -
205171 public function applyQuestion( qp_StubQuestion $question ) {
206172 $this->question_id = $question->mQuestionId;
207173 $this->type = $question->mType;
Index: trunk/extensions/QPoll/model/qp_pollstore.php
@@ -399,17 +399,17 @@
400400 $db = wfGetDB( DB_MASTER );
401401 $rows = qp_PollCache::load( $db, 'qp_ProposalCache' );
402402 # load proposal text from DB
 403+ $prop_attrs = qp_Setup::$propAttrs;
403404 foreach ( $rows as $row ) {
404405 $question_id = $row->question_id;
405406 $proposal_id = $row->proposal_id;
406407 if ( $this->questionExists( $question_id ) ) {
407408 $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;
412413 }
413 - $qdata->ProposalText[$proposal_id] = $prop_text;
414414 }
415415 }
416416 }
@@ -959,6 +959,10 @@
960960 $proposals[$catkey] = $qdata->ProposalCategoryText[ $propkey ][ $id_key ];
961961 }
962962 }
 963+ if ( count( $proposals ) === 0 ) {
 964+ # 'catreq' = 0, pass one single empty cat to the interpretation script
 965+ $proposals[0] = '';
 966+ }
963967 if ( isset( $qdata->ProposalNames[$propkey] ) ) {
964968 $questions[$qdata->ProposalNames[$propkey]] = $proposals;
965969 } else {
Index: trunk/extensions/QPoll/specials/qp_special.php
@@ -48,6 +48,10 @@
4949 if ( self::$linker == null ) {
5050 self::$linker = new Linker();
5151 }
 52+ if ( qp_Setup::$propAttrs === null ) {
 53+ qp_Setup::$propAttrs = new qp_PropAttrs();
 54+ }
 55+
5256 parent::__construct( $name, $restriction, $listed, $function, $file, $includable );
5357 }
5458
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
1144 + native
Index: trunk/extensions/QPoll/ctrl/poll/qp_abstractpoll.php
@@ -78,7 +78,7 @@
7979 * possible xml-like attributes the question may have
8080 */
8181 var $questionAttributeKeys = array(
82 - 't[yi]p[eo]', 'name', 'layout', 'textwidth', 'propwidth', 'showresults'
 82+ 't[yi]p[eo]', 'name', 'catreq', 'layout', 'textwidth', 'propwidth', 'showresults'
8383 );
8484
8585 /**
@@ -89,9 +89,10 @@
9090 * that can be partially merged from poll to question (similar to CSS)
9191 */
9292 var $defaultQuestionAttributes = array(
93 - 'propwidth' => null,
 93+ 'catreq' => null,
9494 'layout' => null,
95 - 'textwidth' => null
 95+ 'textwidth' => null,
 96+ 'propwidth' => null
9697 );
9798
9899 /**
@@ -181,7 +182,7 @@
182183 # quote the params (if any)
183184 $args = array_map( array( 'qp_Setup', 'specialchars' ), $args );
184185 array_unshift( $args, $key );
185 - return call_user_func_array( array( self, 'fatalErrorNoQuote' ), $args );
 186+ return call_user_func_array( array( __CLASS__, 'fatalErrorNoQuote' ), $args );
186187 }
187188
188189 static function s_getPollTitleFragment( $pollid, $dash = '#' ) {
Index: trunk/extensions/QPoll/ctrl/question/qp_abstractquestion.php
@@ -15,19 +15,19 @@
1616 # when the collection of the questions is not sparce (was not randomized)
1717 var $mQuestionId;
1818
19 - var $mState = ''; // current state of question parsing (no error)
 19+ # current state of question parsing (no error)
 20+ var $mState = '';
2021 # default type of the question; stored in DB;
2122 # should always be properly initialized in parent controller via $poll->parseMainHeader()
2223 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 = '';
2624 var $mCategories = array();
2725 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;
3232
3333 # statistics
3434 var $Percents = null;
@@ -94,10 +94,6 @@
9595 $this->view->setPropWidth( $paramkeys[ 'propwidth' ] );
9696 }
9797
98 - function getProposalIdByName( $proposalName ) {
99 - return array_search( $proposalName, $this->mProposalNames, true );
100 - }
101 -
10298 function getPercents( $proposalId, $catId ) {
10399 if ( is_array( $this->Percents ) && array_key_exists( $proposalId, $this->Percents ) &&
104100 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 @@
195195 */
196196 class qp_TextQuestion extends qp_StubQuestion {
197197
 198+ # required count of single proposal categories that should be filled by user
 199+ var $mCatReq = 'all';
198200 # regexp for separation of proposal line tokens
199201 var $propCatPattern;
200202
@@ -257,13 +259,18 @@
258260 */
259261 function applyAttributes( array $paramkeys ) {
260262 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' ) {
262268 # radio button prevents from filling all categories, disable it
263269 if ( ( $radio_brace = array_search( 'radio', $this->input_braces_types, true ) ) !== false ) {
264270 unset( $this->input_braces_types[$radio_brace] );
265271 unset( $this->matching_braces[$radio_brace] );
266272 }
267273 }
 274+ */
268275 $braces_list = array_map( 'preg_quote',
269276 array_merge(
270277 ( array_values( $this->matching_braces ) ),
@@ -461,21 +468,27 @@
462469 $opt = new qp_TextQuestionOptions();
463470 # set static view state for the future qp_TextQuestionProposalView instances
464471 qp_TextQuestionProposalView::applyViewState( $this->view );
 472+ $prop_attrs = qp_Setup::$propAttrs;
465473 foreach ( $this->raws as $raw ) {
466474 $opt->reset();
467475 $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' );
474482 }
475483 $this->dbtokens = $brace_stack = array();
476484 $dbtokens_idx = -1;
477485 $catId = 0;
478486 $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+ );
480493 $matching_closed_brace = '';
481494 $this->findMatchingBraces();
482495 $this->backtrackMismatchingBraces();
@@ -540,41 +553,31 @@
541554 }
542555 # check if there is at least one category defined
543556 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
545558 $this->propview->prependErrorToken( wfMsg( 'qp_error_too_few_categories' ), 'error' );
546559 }
547 - $proposal_text = serialize( $this->dbtokens );
 560+ $prop_attrs->dbText = serialize( $this->dbtokens );
548561 # 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'] ) {
550563 # too long proposal field to store into the DB
551564 # this is very important check for text questions because
552565 # category definitions are stored within the proposal text
553566 $this->propview->prependErrorToken( wfMsg( 'qp_error_too_long_proposal_text' ), 'error' );
554567 }
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;
558571 }
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;
578574 }
 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+ }
579582 $this->view->addProposal( $proposalId, $this->propview );
580583 $proposalId++;
581584 }
Index: trunk/extensions/QPoll/ctrl/question/qp_mixedquestion.php
@@ -10,6 +10,9 @@
1111 */
1212 class qp_MixedQuestion extends qp_TabularQuestion {
1313
 14+ # required count of single proposal categories that should be filled by user
 15+ var $mCatReq = 1;
 16+
1417 /**
1518 * Creates question view which should be renreded and
1619 * also may be altered during the poll generation
@@ -24,55 +27,59 @@
2528 $proposalId = -1;
2629 # set static view state for the future qp_TabularQuestionProposalView instances
2730 qp_TabularQuestionProposalView::applyViewState( $this->view );
 31+ $prop_attrs = qp_Setup::$propAttrs;
2832 foreach ( $this->raws as $raw ) {
 33+ # get proposal attributes
 34+ $prop_attrs->getFromSource( $raw );
2935 # new proposal view
3036 $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
3340 array_shift( $matches ); // remove "at whole" match
3441 $last_matches = $matches;
3542 } else {
3643 if ( $proposalId >= 0 ) {
3744 # 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;
3946 $matches = $last_matches;
4047 }
4148 }
42 - if ( $pview->text === null ) {
 49+ if ( $prop_attrs->dbText === null ) {
4350 continue;
4451 }
 52+ $pview->text = $prop_attrs->dbText;
4553 $proposalId++;
4654 # 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 ) {
4956 $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;
5261 }
53 - $this->mProposalText[$proposalId] = trim( $pview->text );
 62+ $this->mProposalText[$proposalId] = strval( $prop_attrs );
5463 # Determine a type ID, according to the questionType and the number of signes.
5564 foreach ( $this->mCategories as $catId => $catDesc ) {
56 - $typeId = $matches[ $catId ];
 65+ $typeId = $matches[$catId];
5766 # start new input field tag (category)
5867 $pview->addNewCategory( $catId );
5968 $inp = array( '__tag' => 'input' );
6069 # Determine the input's name and value.
 70+ $name = "q{$this->mQuestionId}p{$proposalId}s{$catId}";
6171 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;
7784 }
7885 # Determine if the input has to be checked.
7986 $input_checked = false;
@@ -133,12 +140,16 @@
134141 $pview->setErrorMessage( wfMsg( 'qp_error_proposal_text_empty' ), 'error' );
135142 throw new Exception( 'qp_error' );
136143 }
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
140151 $pview->prependErrorMessage( wfMsg( 'qp_error_no_answer' ), 'NA' );
141152 # if there was no previous errors, hightlight the whole row
142 - if ( $prev_state == '' ) {
 153+ if ( $this->getState() == '' ) {
143154 throw new Exception( 'qp_error' );
144155 }
145156 }
Index: trunk/extensions/QPoll/ctrl/question/qp_tabularquestion.php
@@ -279,22 +279,25 @@
280280 $proposalId = -1;
281281 # set static view state for the future qp_TabularQuestionProposalView instances
282282 qp_TabularQuestionProposalView::applyViewState( $this->view );
 283+ $prop_attrs = qp_Setup::$propAttrs;
283284 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 ) ) {
285288 continue;
286289 }
287290 # new proposal view
288291 $pview = new qp_TabularQuestionProposalView( $proposalId + 1, $this );
289292 $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 ) {
294295 $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;
297300 }
298 - $this->mProposalText[$proposalId] = trim( $pview->text );
 301+ $this->mProposalText[$proposalId] = strval( $prop_attrs );
299302 foreach ( $this->mCategories as $catId => $catDesc ) {
300303 # start new input field tag (category)
301304 $pview->addNewCategory( $catId );
@@ -303,12 +306,12 @@
304307 # Determine the input's name and value.
305308 switch( $this->mType ) {
306309 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}";
309312 break;
310313 case 'singleChoice':
311 - $name = 'q' . $this->mQuestionId . 'p' . $proposalId;
312 - $value = 's' . $catId;
 314+ $name = "q{$this->mQuestionId}p{$proposalId}";
 315+ $value = "s{$catId}";
313316 # category spans have sense only with single choice proposals
314317 $pview->renderSpan( $name, $value, $catDesc );
315318 break;
Index: trunk/extensions/QPoll/ctrl/question/qp_stubquestion.php
@@ -15,6 +15,14 @@
1616 # optional question literal name, used to address questions in interpretation scripts
1717 var $mName = null;
1818
 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+
1927 # current user voting taken from POST data (if available)
2028 var $mProposalCategoryId = Array(); // user true/false answers to the question's proposal
2129 var $mProposalCategoryText = Array(); // user text answers to the question's proposal
@@ -40,10 +48,60 @@
4149 $this->mName = $name;
4250 }
4351
 52+ /**
 53+ * Get question key (reference)
 54+ * @return mixed
 55+ * string question name if available, otherwise
 56+ * integer question id
 57+ */
4458 function getQuestionKey() {
4559 return $this->mName === null ? $this->mQuestionId : $this->mName;
4660 }
4761
 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+
48106 # load some question fields from qp_QuestionData given
49107 # (usually qp_QuestionData is an array property of qp_PollStore instance)
50108 # @param $qdata - an instance of qp_QuestionData
@@ -109,6 +167,19 @@
110168 }
111169
112170 /**
 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+ /**
113184 * @return mixed
114185 * array (associative) of script-generated interpretation error messages
115186 * for current question proposals (and optionally categories);
Index: trunk/extensions/QPoll/qp_user.php
@@ -142,8 +142,11 @@
143143 class qp_Setup {
144144
145145 # internal unique error codes
 146+ const NO_ERROR = 0;
146147 const ERROR_MISSED_TITLE = 1;
147148 const ERROR_INVALID_ADDRESS = 2;
 149+ const ERROR_TOO_LONG_PROPNAME = 3;
 150+ const ERROR_NUMERIC_PROPNAME = 4;
148151
149152 # unicode entity used to display selected checkboxes and radiobuttons in
150153 # result views at Special:Pollresults page
@@ -154,7 +157,7 @@
155158 # matches string which contains integer number in range 1..9999
156159 const PREG_POSITIVE_INT4_MATCH = '/^[1-9]\d{0,3}$/';
157160
158 - ## separators of lines / values for question type 'text' / 'text!'
 161+ ## separators of lines / values for question type="text"
159162 # these should not be the same and should not appear in valid text;
160163 # characters that are used to separate values of select multiple
161164 const SELECT_MULTIPLE_VALUES_SEPARATOR = "\r";
@@ -177,6 +180,9 @@
178181 static $user; // User instance recieved from hook
179182 static $request; // WebRequest instance recieved from hook
180183
 184+ # single instance for extracting attributes from proposal lines
 185+ static $propAttrs = null;
 186+
181187 /**
182188 * The map of question 'type' attribute value to the question's ctrl / view / subtype.
183189 */
@@ -205,12 +211,6 @@
206212 'text' => array(
207213 'ctrl' => 'qp_TextQuestion',
208214 'view' => 'qp_TextQuestionView',
209 - 'mType' => 'textQuestion',
210 - 'mSubType' => 'requireAllCategories'
211 - ),
212 - 'text!' => array(
213 - 'ctrl' => 'qp_TextQuestion',
214 - 'view' => 'qp_TextQuestionView',
215215 'mType' => 'textQuestion'
216216 )
217217 );
@@ -364,6 +364,8 @@
365365 'ctrl/question/qp_mixedquestion.php' => 'qp_MixedQuestion',
366366 'ctrl/question/qp_textquestion.php' => array( 'qp_TextQuestionOptions', 'qp_TextQuestion' ),
367367 'ctrl/question/qp_questionstats.php' => 'qp_QuestionStats',
 368+ # proposal attributes
 369+ 'ctrl/qp_propattrs.php' => 'qp_PropAttrs',
368370 # interpretation results
369371 'ctrl/qp_interpresult.php' => 'qp_InterpResult',
370372
@@ -464,6 +466,7 @@
465467 $wgGroupPermissions['bureaucrat']['editinterpretation'] = true;
466468
467469 $wgDebugLogGroups['qpoll'] = 'qpoll_debug_log.txt';
 470+
468471 }
469472
470473 static function mediaWikiVersionCompare( $version, $operator = '<' ) {
@@ -615,6 +618,7 @@
616619 } elseif ( $request->getVal( 'pollId' ) !== null ) {
617620 self::clearCache();
618621 }
 622+ self::$propAttrs = new qp_PropAttrs();
619623 return true;
620624 }
621625
Index: trunk/extensions/QPoll/view/proposal/qp_textquestionproposalview.php
@@ -10,6 +10,8 @@
1111 */
1212 class qp_TextQuestionProposalView extends qp_StubQuestionProposalView {
1313
 14+ # count of required categories to be non-empty in current proposal
 15+ var $catreq = 'all';
1416 # list of viewtokens
1517 # elements of string type contain proposal parts;
1618 # elements of stdClass :
Index: trunk/extensions/QPoll/view/question/qp_textquestionview.php
@@ -343,23 +343,24 @@
344344 }
345345
346346 /**
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
352353 * @return tagarray
353354 */
354 - function renderParsedProposal( $pkey, array &$viewtokens ) {
 355+ function renderParsedProposal( $pkey, qp_TextQuestionProposalView $propview ) {
355356 $vr = $this->vr;
356357 # proposal prefix for category tag id generation
357358 $vr->reset( "tx{$this->ctrl->poll->mOrderId}q{$this->ctrl->mQuestionId}p{$pkey}" );
358 - foreach ( $viewtokens as $elem ) {
 359+ foreach ( $propview->viewtokens as $elem ) {
359360 $vr->cell = array();
360361 if ( is_object( $elem ) ) {
361362 if ( isset( $elem->options ) ) {
362363 $className = 'cat_part';
363 - if ( $this->ctrl->mSubType === 'requireAllCategories' && $elem->unanswered ) {
 364+ if ( $propview->catreq === 'all' && $elem->unanswered ) {
364365 $className .= ' cat_noanswer';
365366 }
366367 if ( isset( $elem->interpError ) ) {
@@ -423,7 +424,7 @@
424425 qp_Renderer::addRow( $questionTable, $row, $rowattrs, 'th', $attribute_maps );
425426 }
426427 foreach ( $this->pviews as $pkey => $propview ) {
427 - $prop = $this->renderParsedProposal( $pkey, $propview->viewtokens );
 428+ $prop = $this->renderParsedProposal( $pkey, $propview );
428429 $rowattrs = array( 'class' => $propview->rowClass );
429430 if ( $this->transposed ) {
430431 qp_Renderer::addColumn( $questionTable, $prop, $rowattrs );

Status & tagging log