r99779 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r99778‎ | r99779 | r99780 >
Date:16:05, 14 October 2011
Author:questpc
Status:deferred
Tags:
Comment:
Text question layout allows to display proposal parts / categories either inline or in table cells. In last case, text question optionally can be transposed. Fixed and explained last message from recent commits.
Modified paths:
  • /trunk/extensions/QPoll/ctrl/poll/qp_abstractpoll.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/qp_user.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,7 +126,7 @@
127127 'qp_error_no_answer' => 'Unanswered proposal.',
128128 'qp_error_unique' => 'Question of type unique() has more proposals than possible answers defined: Impossible to complete.',
129129 'qp_error_no_more_attempts' => 'You have reached maximal number of submitting attempts for this poll.',
130 - 'qp_error_no_interpretation' => 'Interpretation script does not exist',
 130+ 'qp_error_no_interpretation' => 'Interpretation script does not exist.',
131131 'qp_error_interpretation_no_return' => 'Interpretation script returned no result.',
132132 'qp_error_structured_interpretation_is_too_long' => 'Structured interpretation is too long to be stored in database. Please correct your interpretation script.',
133133 'qp_error_no_json_decode' => 'Interpretation of poll answers requires json_decode() PHP function.',
@@ -190,6 +190,7 @@
191191 * $3 is the poll ID of the poll, which this erroneous poll depends on.',
192192 'qp_error_too_many_spans' => 'There cannot be more category groups defined than the total count of subcategories.',
193193 'qp_error_too_few_spans' => 'Every category group should include at least two subcategories',
 194+ 'qp_error_no_interpretation' => 'Title of interpretation script was specified in poll header, however no article was found with that title. Either remove "interpretation" xml attribute of poll or create the title specified by "interpretation" attribute.',
194195 'qp_error_interpretation_no_return' => 'Interpretation script missed an return statement.',
195196 'qp_error_structured_interpretation_is_too_long' => "Structured interpretation is serialized string containing scalar value or an associative array stored into database table field. It's purpose is to have measurable, easily processable interpretation result for the particular poll which then can be processed by external tools (via XLS export) or, to be read and processed by next poll interpretation script (data import and in the future maybe an export as well). When the serialized string is too long, it should never be stored, otherwise it will be truncated by DBMS so it cannot be properly unserialized later.",
196197 'qp_error_eval_missed_lang_attr' => '{{doc-important|Do not translate "lang" as it is the name of an XML attribute that is not localised.}}',
@@ -1132,7 +1133,7 @@
11331134 'qp_error_no_answer' => 'Proposition sans réponse',
11341135 'qp_error_unique' => 'La question de type unique() a plus de propositions qu’il n’y a de réponses possibles définies : impossible de compléter',
11351136 'qp_error_no_more_attempts' => 'Vous avez atteint le nombre maximal de tentatives de soumission pour ce sondage.',
1136 - 'qp_error_no_interpretation' => "Le script d'interprétation n'existe pas",
 1137+ 'qp_error_no_interpretation' => "Le script d'interprétation n'existe pas.",
11371138 'qp_error_interpretation_no_return' => "Le script d'interprétation n'a renvoyé aucun résultat.",
11381139 'qp_error_structured_interpretation_is_too_long' => "L'interprétation structurée est trop longue pour être stockée dans la base de données. Merci de corriger votre script d'interprétation.",
11391140 'qp_error_no_json_decode' => "L'interprétation des réponses au sondage nécessite la fonction PHP json_decode().",
@@ -1278,7 +1279,7 @@
12791280 'qp_error_no_answer' => 'Proposta sen resposta',
12801281 'qp_error_unique' => 'A pregunta de tipo unique() ten definidas máis propostas que respostas posibles: imposible de completar',
12811282 'qp_error_no_more_attempts' => 'Alcanzou o número máximo de intentos de envío para esta enquisa.',
1282 - 'qp_error_no_interpretation' => 'A escritura de interpretación non existe',
 1283+ 'qp_error_no_interpretation' => 'A escritura de interpretación non existe.',
12831284 'qp_error_interpretation_no_return' => 'A escritura de interpretación non devolveu resultados.',
12841285 'qp_error_structured_interpretation_is_too_long' => 'A interpretación estruturada é longa de máis para almacenala na base de datos. Corrixa a súa escritura de interpretación.',
12851286 'qp_error_no_json_decode' => 'A interpretación das respostas ás enquisas necesitan a función PHP json_decode().',
@@ -1659,7 +1660,7 @@
16601661 'qp_error_no_answer' => 'Proposition sin responsa',
16611662 'qp_error_unique' => 'Pro le question de typo unique() es definite plus propositiones que responsas possibile: non pote completar',
16621663 'qp_error_no_more_attempts' => 'Tu ha attingite le numero maxime de tentativas de submission pro iste sondage',
1663 - 'qp_error_no_interpretation' => 'Le script de interpretation non existe',
 1664+ 'qp_error_no_interpretation' => 'Le script de interpretation non existe.',
16641665 'qp_error_interpretation_no_return' => 'Le script de interpretation non retornava resultatos',
16651666 'qp_error_structured_interpretation_is_too_long' => 'Le interpretation structurate es troppo longe pro immagazinar lo in le base de datos. Per favor corrige le script de interpretation.',
16661667 'qp_error_no_json_decode' => 'Le interpretation del responsas al sondage require le function PHP json_decode()',
Index: trunk/extensions/QPoll/ctrl/poll/qp_abstractpoll.php
@@ -75,6 +75,13 @@
7676 var $pollStore = null;
7777
7878 /**
 79+ * possible xml-like attributes the question may have
 80+ */
 81+ var $questionAttributeKeys = array(
 82+ 't[yi]p[eo]', 'layout', 'textwidth', 'propwidth', 'showresults'
 83+ );
 84+
 85+ /**
7986 * default values of 'propwidth', 'textwidth' and 'layout' attributes
8087 * will be applied to child questions that do not have these attributes defined
8188 *
@@ -232,12 +239,7 @@
233240 * @return string the value of question's type attribute
234241 */
235242 function getQuestionAttributes( $attr_str, &$paramkeys ) {
236 - $paramkeys = array( 't[yi]p[eo]' => null, 'layout' => null, 'textwidth' => null, 'propwidth' => null, 'showresults' => null );
237 - $match = array();
238 - foreach ( $paramkeys as $key => $val ) {
239 - preg_match( '`' . $key . '\s?=\s?"(.*?)"`u', $attr_str, $match );
240 - $paramkeys[$key] = ( count( $match ) > 1 ) ? $match[1] : null;
241 - }
 243+ $paramkeys = qp_Setup::getXmlLikeAttributes( $attr_str, $this->questionAttributeKeys );
242244 # apply default questions attributes from poll definition, if there is any
243245 foreach ( $this->defaultQuestionAttributes as $attr => $val ) {
244246 if ( $paramkeys[$attr] === null ) {
Index: trunk/extensions/QPoll/ctrl/question/qp_textquestion.php
@@ -6,22 +6,41 @@
77
88 /**
99 * Stores the list of current category options -
10 - * usually the pipe-separated entries in double angle brackets list
 10+ * usually the pipe-separated entries in specified brackets list
1111 */
1212 class qp_TextQuestionOptions {
1313
1414 # boolean, indicates whether incoming tokens are category list elements
1515 var $isCatDef;
 16+ # type of created element (text,radio,checkbox)
 17+ var $type;
1618 # counter of pipe-separated elements in-between << >> markup
17 - # used to distinguish real category options from textwidth definition
 19+ # used to distinguish real category options from attributes definition
 20+ # for type='text'
1821 var $catDefIdx;
1922 # list of input options; array whose every element is a string
2023 var $input_options;
21 - # a value of textwidth definition for input text field
22 - # it is defined as first element of options list, for example:
23 - # <<::12>> or <<::15|test>>
24 - # currently, it is used only for text inputs (not for select/option list)
25 - var $textwidth;
 24+
 25+ # whether the current option has xml-like attributes specified
 26+ var $hasAttributes = false;
 27+ var $attributes = array(
 28+ ## a value of input text field width in 'em'
 29+ # possible values: null, positive int
 30+ # defined as first element xml-like attribute of options list, for example:
 31+ # <<:: width="12">> or <<:: width="15"|test>>
 32+ # currently, it is used only for text inputs (not for select/option list)
 33+ 'width' => null,
 34+ ## whether the text options of current category has to be sorted;
 35+ # possible values: null (do not sort), 'asc', 'desc'
 36+ # defined as first element xml-like attribute of options list, for example:
 37+ # <<:: sorting="desc"|a|b|c>>
 38+ 'sorting' => null,
 39+ ## whether the checkbox type option of current category has to be checked by default;
 40+ # possible value: null (not checked), not null (checked)
 41+ # defined as first element xml-like attribute of options list, for example:
 42+ # <[checked=""]>
 43+ 'checked' => null
 44+ );
2645 # a pointer to last element in $this->input_options array
2746 var $iopt_last;
2847
@@ -39,10 +58,15 @@
4059 * Applies default settings to the options list
4160 * New category begins
4261 */
43 - function startOptionsList() {
 62+ function startOptionsList( $type ) {
4463 $this->isCatDef = true;
 64+ $this->type = $type;
4565 $this->input_options = array( 0 => '' );
46 - $this->textwidth = null; // will use default value
 66+ $this->hasAttributes = false;
 67+ # set default values of xml-like attributes
 68+ foreach ( $this->attributes as $attr_name => &$attr_val ) {
 69+ $attr_val = null;
 70+ }
4771 $this->iopt_last = &$this->input_options[0];
4872 }
4973
@@ -51,33 +75,54 @@
5276 * This option will be "current last option"
5377 */
5478 function addEmptyOption() {
55 - # add new empty option only if there was no textwidth definition
56 - if ( is_null( $this->textwidth ) || $this->catDefIdx !== 0 ) {
57 - # add new empty option to the end of the list
58 - $this->input_options[] = '';
59 - $this->iopt_last = &$this->input_options[count( $this->input_options ) - 1];
 79+ # new options are meaningful only for type 'text'
 80+ if ( $this->type === 'text' ) {
 81+ # add new empty option only if there was no xml attributes definition
 82+ if ( !$this->hasAttributes || $this->catDefIdx !== 0 ) {
 83+ # add new empty option to the end of the list
 84+ $this->input_options[] = '';
 85+ $this->iopt_last = &$this->input_options[count( $this->input_options ) - 1];
 86+ }
 87+ $this->catDefIdx++;
6088 }
61 - $this->catDefIdx++;
6289 }
6390
6491 /**
65 - * Set string value to current last option
 92+ * Add string part to value of current last option
6693 * @param $token string current value of token between pipe separators
67 - * Also, _optionally_ overrides textwidth property
 94+ * Also, _optionally_ parses xml-like attributes (when these are found in category definition)
6895 */
6996 function addToLastOption( $token ) {
70 - # first entry of category options might be definition of
71 - # the current category input textwidth instead
7297 $matches = array();
73 - if ( count( $this->input_options ) === 1 &&
74 - preg_match( '`^\s*::(\d{1,2})\s*$`', $token, $matches ) &&
75 - $matches[1] > 0 ) {
76 - # override the textwidth of input options
77 - $this->textwidth = intval( $matches[1] );
78 - } else {
79 - # add new input option
80 - $this->iopt_last .= $token;
 98+ if ( $this->type === 'text' ) {
 99+ # first entry of "category type text" might contain current category
 100+ # xml-like attributes
 101+ if ( count( $this->input_options ) === 1 &&
 102+ preg_match( '`^::\s*(.+)$`', $token, $matches ) ) {
 103+ # note that hasAttributes is always true regardless the attributes are used or not,
 104+ # because it is checked in $this->addEmptyOption()
 105+ $this->hasAttributes = true;
 106+ # parse attributes string
 107+ $option_attributes = qp_Setup::getXmlLikeAttributes( $matches[1], array( 'width', 'sorting' ) );
 108+ # apply attributes to current option
 109+ foreach ( $option_attributes as $attr_name => $attr_val ) {
 110+ $this->attributes[$attr_name] = $attr_val;
 111+ }
 112+ return;
 113+ }
 114+ } elseif ( $this->type === 'checkbox' ) {
 115+ if ( $token !== '' ) {
 116+ # checkbox type of categories do not contain text values,
 117+ # only xml-like attributes
 118+ $option_attributes = qp_Setup::getXmlLikeAttributes( $token, array( 'checked' ) );
 119+ # apply attributes to current option
 120+ foreach ( $option_attributes as $attr_name => $attr_val ) {
 121+ $this->attributes[$attr_name] = $attr_val;
 122+ }
 123+ }
81124 }
 125+ # add new input option
 126+ $this->iopt_last .= $token;
82127 }
83128
84129 /**
@@ -92,6 +137,14 @@
93138 # make sure unique elements keys are consequitive starting from 0
94139 $this->input_options[] = $option;
95140 }
 141+ switch ( $this->attributes['sorting'] ) {
 142+ case 'asc' :
 143+ sort( $this->input_options, SORT_STRING );
 144+ break;
 145+ case 'desc' :
 146+ rsort( $this->input_options, SORT_STRING );
 147+ break;
 148+ }
96149 }
97150
98151 } /* end of qp_TextQuestionOptions class */
@@ -105,7 +158,8 @@
106159 */
107160 class qp_TextQuestion extends qp_StubQuestion {
108161
109 - const PROP_CAT_PATTERN = '`(<<|>>|{{|}}|\[\[|\]\]|\|)`u';
 162+ # regexp for separation of proposal line tokens
 163+ static $propCatPattern = null;
110164
111165 # $propview is an instance of qp_TextQuestionProposalView
112166 # which contains parsed tokens for combined
@@ -117,7 +171,48 @@
118172 # only proposal parts and category options
119173 var $dbtokens = array();
120174
 175+ # list of opening input braces types
 176+ static $input_braces_types = array(
 177+ '<<' => 'text',
 178+ '<(' => 'radio',
 179+ '<[' => 'checkbox'
 180+ );
 181+ # matches of opening / closing braces
 182+ static $matching_braces = array(
 183+ # wiki link
 184+ '[[' => ']]',
 185+ # wiki magicword
 186+ '{{' => '}}',
 187+ # text input / select option
 188+ '<<' => '>>',
 189+ # radiobutton
 190+ '<(' => ')>',
 191+ # checkbox
 192+ '<[' => ']>'
 193+ );
 194+
121195 /**
 196+ * Constructor
 197+ * @public
 198+ * @param $poll an instance of question's parent controller
 199+ * @param $view an instance of question view "linked" to this question
 200+ * @param $questionId the identifier of the question used to generate input names
 201+ */
 202+ function __construct( qp_AbstractPoll $poll, qp_StubQuestionView $view, $questionId ) {
 203+ parent::__construct( $poll, $view, $questionId );
 204+ if ( self::$propCatPattern === null ) {
 205+ $braces_list = array_map( 'preg_quote',
 206+ array_merge(
 207+ ( array_values( self::$matching_braces ) ),
 208+ array_keys( self::$matching_braces ),
 209+ array( '|' )
 210+ )
 211+ );
 212+ self::$propCatPattern = '/(' . implode( '|', $braces_list ) . ')/u';
 213+ }
 214+ }
 215+
 216+ /**
122217 * Parses question body header.
123218 * Text questions do not have "body header" (no definitions of spans and categories)
124219 * so, this method just splits raw lines of body text to analyze raws in $this->parseBody()
@@ -167,11 +262,6 @@
168263 * also may be altered during the poll generation
169264 */
170265 function parseBody() {
171 - $matching_braces = array(
172 - '[[' => ']]',
173 - '{{' => '}}',
174 - '<<' => '>>'
175 - );
176266 $proposalId = 0;
177267 # Currently, we use just a single instance (no nested categories)
178268 $opt = new qp_TextQuestionOptions();
@@ -185,63 +275,70 @@
186276 $this->dbtokens = $brace_stack = array();
187277 $catId = 0;
188278 $last_brace = '';
189 - $tokens = preg_split( self::PROP_CAT_PATTERN, $raw, -1, PREG_SPLIT_DELIM_CAPTURE );
 279+ $tokens = preg_split( self::$propCatPattern, $raw, -1, PREG_SPLIT_DELIM_CAPTURE );
 280+ $matching_closed_brace = '';
190281 foreach ( $tokens as $token ) {
191 - $isContinue = false;
192 - switch ( $token ) {
193 - case '|' :
194 - if ( $opt->isCatDef ) {
195 - if ( count( $brace_stack ) == 1 && $brace_stack[0] === '>>' ) {
196 - # pipe char starts new option only at top brace level,
197 - # with angled braces
198 - $opt->addEmptyOption();
199 - $isContinue = true;
 282+ try {
 283+ # $toBeStored == true when current $token has to be stored into
 284+ # category / proposal list (depending on $opt->isCatDef)
 285+ $toBeStored = true;
 286+ if ( $token === '|' ) {
 287+ # parameters separator
 288+ if ( $opt->isCatDef ) {
 289+ if ( count( $brace_stack ) == 1 && $brace_stack[0] === $matching_closed_brace ) {
 290+ # pipe char starts new option only at top brace level,
 291+ # with matching input brace
 292+ $opt->addEmptyOption();
 293+ $toBeStored = false;
 294+ }
200295 }
201 - }
202 - break;
203 - case '[[' :
204 - case '{{' :
205 - case '<<' :
206 - array_push( $brace_stack, $matching_braces[$token] );
207 - if ( $token === '<<' && count( $brace_stack ) == 1 ) {
208 - $opt->startOptionsList();
209 - $isContinue = true;
210 - }
211 - break;
212 - case ']]' :
213 - case '}}' :
214 - case '>>' :
215 - if ( count( $brace_stack ) > 0 ) {
216 - $last_brace = array_pop( $brace_stack );
217 - if ( $last_brace != $token ) {
218 - array_push( $brace_stack, $last_brace );
219 - break;
 296+ } elseif ( array_key_exists( $token, self::$matching_braces ) ) {
 297+ # opening braces
 298+ array_push( $brace_stack, self::$matching_braces[$token] );
 299+ if ( array_key_exists( $token, self::$input_braces_types ) &&
 300+ count( $brace_stack ) == 1 ) {
 301+ # start category definiton
 302+ $matching_closed_brace = self::$matching_braces[$token];
 303+ $opt->startOptionsList( self::$input_braces_types[$token] );
 304+ $toBeStored = false;
220305 }
221 - if ( count( $brace_stack ) > 0 || $token !== '>>' ) {
222 - break;
 306+ } elseif ( in_array( $token, self::$matching_braces ) ) {
 307+ # closing braces
 308+ if ( count( $brace_stack ) > 0 ) {
 309+ $last_brace = array_pop( $brace_stack );
 310+ if ( $last_brace != $token ) {
 311+ array_push( $brace_stack, $last_brace );
 312+ throw new Exception( 'break' );
 313+ }
 314+ if ( count( $brace_stack ) > 0 || $token !== $matching_closed_brace ) {
 315+ throw new Exception( 'break' );
 316+ }
 317+ $matching_closed_brace = '';
 318+ # add new category input options for the storage
 319+ $this->dbtokens[] = $opt->input_options;
 320+ # setup mCategories
 321+ $this->mCategories[$catId] = array( 'name' => strval( $catId ) );
 322+ # load proposal/category answer (when available)
 323+ $this->loadProposalCategory( $opt, $proposalId, $catId );
 324+ # current category is over
 325+ $catId++;
 326+ $toBeStored = false;
223327 }
224 - # add new category input options for the storage
225 - $this->dbtokens[] = $opt->input_options;
226 - # setup mCategories
227 - $this->mCategories[$catId] = array( 'name' => strval( $catId ) );
228 - # load proposal/category answer (when available)
229 - $this->loadProposalCategory( $opt, $proposalId, $catId );
230 - # current category is over
231 - $catId++;
232 - $isContinue = true;
233328 }
234 - break;
 329+ } catch ( Exception $e ) {
 330+ if ( $e->getMessage() !== 'break' ) {
 331+ throw new MWException( $e->getMessage() );
 332+ }
235333 }
236 - if ( $isContinue ) {
237 - continue;
 334+ if ( $toBeStored ) {
 335+ if ( $opt->isCatDef ) {
 336+ $opt->addToLastOption( $token );
 337+ } else {
 338+ # add new proposal part
 339+ $this->dbtokens[] = strval( $token );
 340+ $this->propview->addProposalPart( $token );
 341+ }
238342 }
239 - if ( $opt->isCatDef ) {
240 - $opt->addToLastOption( $token );
241 - } else {
242 - # add new proposal part
243 - $this->dbtokens[] = strval( $token );
244 - $this->propview->addProposalPart( $token );
245 - }
246343 }
247344 # check if there is at least one category defined
248345 if ( $catId === 0 ) {
Index: trunk/extensions/QPoll/qp_user.php
@@ -444,6 +444,23 @@
445445 return $username;
446446 }
447447
 448+ /**
 449+ * Parse string with XML-like attributes (no tag, only attributes)
 450+ * @param $attr_str attribute string
 451+ * @param $attr_list list of XML attributes, PCRE allowed
 452+ * @return array key is attribute regexp
 453+ * value is the value of attribute or null
 454+ */
 455+ static function getXmlLikeAttributes( $attr_str, $attr_list ) {
 456+ $attr_vals = array();
 457+ $match = array();
 458+ foreach ( $attr_list as $attr_name ) {
 459+ preg_match( '/' . $attr_name . '\s?=\s?"(.*?)"/u', $attr_str, $match );
 460+ $attr_vals[$attr_name] = ( count( $match ) > 1 ) ? $match[1] : null;
 461+ }
 462+ return $attr_vals;
 463+ }
 464+
448465 static function onLoadAllMessages() {
449466 if ( !self::$messagesLoaded ) {
450467 self::$messagesLoaded = true;
Index: trunk/extensions/QPoll/view/proposal/qp_textquestionproposalview.php
@@ -16,15 +16,25 @@
1717 # property 'options' indicates current category options list
1818 # property 'error' indicates error message
1919 var $viewtokens = array();
 20+ var $lastTokenType = '';
2021
2122 /**
2223 * Add new proposal part (between two categories or line bounds)
2324 * It is just an element of string type
2425 *
25 - * @param $prop string proposal part
 26+ * @param $token string proposal part
2627 */
27 - function addProposalPart( $prop ) {
28 - $this->viewtokens[] = $prop;
 28+ function addProposalPart( $token ) {
 29+ if ( $this->lastTokenType === 'proposal' ) {
 30+ # add to already existing proposal part
 31+ $last_prop = array_pop( $this->viewtokens );
 32+ $last_prop .= $token;
 33+ array_push( $this->viewtokens, $last_prop );
 34+ return;
 35+ }
 36+ # start new proposal part
 37+ $this->viewtokens[] = $token;
 38+ $this->lastTokenType = 'proposal';
2939 }
3040
3141 /**
@@ -39,27 +49,26 @@
4050 */
4151 function addCatDef( qp_TextQuestionOptions $opt, $name, $text_answer, $unanswered ) {
4252 # $catdef instanceof stdClass properties:
 53+ # property 'type' contains type of current category: 'text', 'checkbox', 'radio'
4354 # property 'options' stores an array of user options
4455 # Multiple options will be selected from the list
4556 # Single option will be displayed as text input
4657 # property 'name' contains name of input element
4758 # property 'value' contains value previousely chosen
4859 # by user (if any)
49 - # property 'textwidth' may optionally override default
50 - # text input width
 60+ # property 'attributes' contain extra atttibutes of current category definition
5161 # property 'unanswered' boolean
5262 # true - the question was POSTed but category is unanswered
5363 # false - the question was not POSTed or category is answered
54 - $catdef = (object) array(
 64+ $this->viewtokens[] = (object) array(
 65+ 'type' => $opt->type,
5566 'options' => $opt->input_options,
5667 'name' => $name,
5768 'value' => $text_answer,
58 - 'unanswered' => $unanswered
 69+ 'unanswered' => $unanswered,
 70+ 'attributes' => $opt->attributes
5971 );
60 - if ( !is_null( $opt->textwidth ) ) {
61 - $catdef->textwidth = $opt->textwidth;
62 - }
63 - $this->viewtokens[] = $catdef;
 72+ $this->lastTokenType = 'category';
6473 }
6574
6675 /**
@@ -78,6 +87,9 @@
7988 if ( $errmsg !== '' ) {
8089 array_unshift( $this->viewtokens, (object) array( 'error'=> $errmsg ) );
8190 }
 91+ if ( count( $this->viewtokens ) < 2 ) {
 92+ $this->lastTokenType = 'errmsg';
 93+ }
8294 }
8395
8496 /**
Index: trunk/extensions/QPoll/view/question/qp_textquestionview.php
@@ -38,6 +38,44 @@
3939 }
4040
4141 /**
 42+ * Proposal / category view row building helper.
 43+ * Currently the single instance is re-used (no nesting).
 44+ */
 45+class qp_TextQuestionViewRow {
 46+
 47+ # each element of row is real table cell or "cell" with spans,
 48+ # depending on $this->tabularDisplay value
 49+ var $row;
 50+ # tagarray with error elements will be merged into adjascent cells
 51+ var $error;
 52+ # tagarray with current cell builded for row
 53+ # cell contains one or multiple tags, describing proposal part or category
 54+ var $cell;
 55+
 56+ function __construct() {
 57+ $this->reset();
 58+ }
 59+
 60+ function reset() {
 61+ $this->row = array();
 62+ $this->error = array();
 63+ $this->cell = array();
 64+ }
 65+
 66+ function addCell() {
 67+ if ( count( $this->error ) > 0 ) {
 68+ # merge previous errors to current cell
 69+ $this->cell = array_merge( $this->error, $this->cell );
 70+ $this->error = array();
 71+ }
 72+ if ( count( $this->cell ) > 0 ) {
 73+ $this->row[] = $this->cell;
 74+ }
 75+ }
 76+
 77+} /* end of qp_TextQuestionViewRow class */
 78+
 79+/**
4280 * Stores question proposals views (see qp_textqestion.php) and
4381 * allows to modify these for results of quizes at the later stage (see qp_poll.php)
4482 * An attempt to make somewhat cleaner question view
@@ -45,7 +83,20 @@
4684 */
4785 class qp_TextQuestionView extends qp_StubQuestionView {
4886
 87+ ## the layout of question
 88+ # true: categories and proposal parts will be placed into
 89+ # table cells (display:table-cell)
 90+ # false: categories and proposal parts will be placed into
 91+ # spans (display:inline)
 92+ var $tabularDisplay = false;
 93+ # whether the resulting display table should be transposed
 94+ # meaningful only when $this->tabularDisplay is true
 95+ var $transposed = false;
 96+
 97+ # default style of text input
4998 var $textInputStyle = '';
 99+ # view row
 100+ var $vr;
50101
51102 /**
52103 * @param $parser
@@ -54,6 +105,7 @@
55106 */
56107 function __construct( &$parser, &$frame, $showResults ) {
57108 parent::__construct( $parser, $frame );
 109+ $this->vr = new qp_TextQuestionViewRow();
58110 /* todo: implement showResults */
59111 }
60112
@@ -62,7 +114,10 @@
63115 }
64116
65117 function setLayout( $layout, $textwidth ) {
66 - /* todo: implement vertical layout */
 118+ if ( $layout !== null ) {
 119+ $this->tabularDisplay = strpos( $layout, 'tabular' ) !== false;
 120+ $this->transposed = strpos( $layout, 'transpose' ) !== false;
 121+ }
67122 if ( $textwidth !== null ) {
68123 $textwidth = intval( $textwidth );
69124 if ( $textwidth > 0 ) {
@@ -127,8 +182,10 @@
128183 * @return tagarray
129184 */
130185 function renderParsedProposal( &$viewtokens ) {
131 - $row = array();
 186+ $vr = $this->vr;
 187+ $vr->reset();
132188 foreach ( $viewtokens as $elem ) {
 189+ $vr->cell = array();
133190 if ( is_object( $elem ) ) {
134191 if ( isset( $elem->options ) ) {
135192 $className = 'cat_part';
@@ -138,7 +195,7 @@
139196 if ( isset( $elem->interpError ) ) {
140197 $className = 'cat_noanswer';
141198 # create view for proposal/category error message
142 - $row[] = array(
 199+ $vr->cell[] = array(
143200 '__tag' => 'span',
144201 'class' => 'proposalerror',
145202 $elem->interpError
@@ -146,7 +203,7 @@
147204 }
148205 # create view for the input options part
149206 if ( count( $elem->options ) === 1 ) {
150 - # one option produces html text input
 207+ # one option produces html text / radio / checkbox input
151208 $value = $elem->value;
152209 # check, whether the definition of category has "pre-filled" value
153210 # single, non-unanswered, non-empty option is a pre-filled value
@@ -158,19 +215,23 @@
159216 $input = array(
160217 '__tag' => 'input',
161218 'class' => $className,
162 - 'type' => 'text',
 219+ 'type' => $elem->type,
163220 'name' => $elem->name,
164221 'value' => qp_Setup::specialchars( $value )
165222 );
 223+ if ( $elem->attributes['checked'] !== null ) {
 224+ $input['checked'] = 'checked';
 225+ }
166226 if ( $this->textInputStyle != '' ) {
167227 # apply poll's textwidth attribute
168228 $input['style'] = $this->textInputStyle;
169229 }
170 - if ( isset( $elem->textwidth ) ) {
171 - # apply current category textwidth "option"
172 - $input['style'] = 'width:' . intval( $elem->textwidth ) . 'em;';
 230+ if ( $elem->attributes['width'] !== null ) {
 231+ # apply current category width attribute
 232+ $input['style'] = 'width:' . intval( $elem->attributes['width'] ) . 'em;';
173233 }
174 - $row[] = $input;
 234+ $vr->cell[] = $input;
 235+ $vr->addCell();
175236 continue;
176237 }
177238 # multiple options produce html select / options
@@ -190,15 +251,16 @@
191252 }
192253 $html_options[] = $html_option;
193254 }
194 - $row[] = array(
 255+ $vr->cell[] = array(
195256 '__tag' => 'select',
196257 'class' => $className,
197258 'name' => $elem->name,
198259 $html_options
199260 );
 261+ $vr->addCell();
200262 } elseif ( isset( $elem->error ) ) {
201263 # create view for proposal/category error message
202 - $row[] = array(
 264+ $vr->error[] = array(
203265 '__tag' => 'span',
204266 'class' => 'proposalerror',
205267 $elem->error
@@ -208,14 +270,21 @@
209271 }
210272 } else {
211273 # create view for the proposal part
212 - $row[] = array(
 274+ $vr->cell[] = array(
213275 '__tag' => 'span',
214276 'class' => 'prop_part',
215277 $this->rtp( $elem )
216278 );
 279+ $vr->addCell();
217280 }
218281 }
219 - return array( $row );
 282+ $vr->cell = array();
 283+ # make sure last "error" tokens are added, if any:
 284+ $vr->addCell();
 285+ if ( $this->tabularDisplay ) {
 286+ return $vr->row;
 287+ }
 288+ return array( $vr->row );
220289 }
221290
222291 /**
@@ -239,7 +308,11 @@
240309 foreach ( $this->pviews as &$propview ) {
241310 $prop = $this->renderParsedProposal( $propview->viewtokens );
242311 $rowattrs = array( 'class' => $propview->rowClass );
243 - qp_Renderer::addRow( $questionTable, $prop, $rowattrs );
 312+ if ( $this->transposed ) {
 313+ qp_Renderer::addColumn( $questionTable, $prop, $rowattrs );
 314+ } else {
 315+ qp_Renderer::addRow( $questionTable, $prop, $rowattrs );
 316+ }
244317 }
245318 return $questionTable;
246319 }

Status & tagging log