Index: trunk/extensions/QPoll/i18n/qp.i18n.php |
— | — | @@ -126,7 +126,7 @@ |
127 | 127 | 'qp_error_no_answer' => 'Unanswered proposal.', |
128 | 128 | 'qp_error_unique' => 'Question of type unique() has more proposals than possible answers defined: Impossible to complete.', |
129 | 129 | '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.', |
131 | 131 | 'qp_error_interpretation_no_return' => 'Interpretation script returned no result.', |
132 | 132 | 'qp_error_structured_interpretation_is_too_long' => 'Structured interpretation is too long to be stored in database. Please correct your interpretation script.', |
133 | 133 | 'qp_error_no_json_decode' => 'Interpretation of poll answers requires json_decode() PHP function.', |
— | — | @@ -190,6 +190,7 @@ |
191 | 191 | * $3 is the poll ID of the poll, which this erroneous poll depends on.', |
192 | 192 | 'qp_error_too_many_spans' => 'There cannot be more category groups defined than the total count of subcategories.', |
193 | 193 | '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.', |
194 | 195 | 'qp_error_interpretation_no_return' => 'Interpretation script missed an return statement.', |
195 | 196 | '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.", |
196 | 197 | '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 @@ |
1133 | 1134 | 'qp_error_no_answer' => 'Proposition sans réponse', |
1134 | 1135 | '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', |
1135 | 1136 | '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.", |
1137 | 1138 | 'qp_error_interpretation_no_return' => "Le script d'interprétation n'a renvoyé aucun résultat.", |
1138 | 1139 | '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.", |
1139 | 1140 | 'qp_error_no_json_decode' => "L'interprétation des réponses au sondage nécessite la fonction PHP json_decode().", |
— | — | @@ -1278,7 +1279,7 @@ |
1279 | 1280 | 'qp_error_no_answer' => 'Proposta sen resposta', |
1280 | 1281 | 'qp_error_unique' => 'A pregunta de tipo unique() ten definidas máis propostas que respostas posibles: imposible de completar', |
1281 | 1282 | '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.', |
1283 | 1284 | 'qp_error_interpretation_no_return' => 'A escritura de interpretación non devolveu resultados.', |
1284 | 1285 | '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.', |
1285 | 1286 | 'qp_error_no_json_decode' => 'A interpretación das respostas ás enquisas necesitan a función PHP json_decode().', |
— | — | @@ -1659,7 +1660,7 @@ |
1660 | 1661 | 'qp_error_no_answer' => 'Proposition sin responsa', |
1661 | 1662 | 'qp_error_unique' => 'Pro le question de typo unique() es definite plus propositiones que responsas possibile: non pote completar', |
1662 | 1663 | '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.', |
1664 | 1665 | 'qp_error_interpretation_no_return' => 'Le script de interpretation non retornava resultatos', |
1665 | 1666 | '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.', |
1666 | 1667 | '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 @@ |
76 | 76 | var $pollStore = null; |
77 | 77 | |
78 | 78 | /** |
| 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 | + /** |
79 | 86 | * default values of 'propwidth', 'textwidth' and 'layout' attributes |
80 | 87 | * will be applied to child questions that do not have these attributes defined |
81 | 88 | * |
— | — | @@ -232,12 +239,7 @@ |
233 | 240 | * @return string the value of question's type attribute |
234 | 241 | */ |
235 | 242 | 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 ); |
242 | 244 | # apply default questions attributes from poll definition, if there is any |
243 | 245 | foreach ( $this->defaultQuestionAttributes as $attr => $val ) { |
244 | 246 | if ( $paramkeys[$attr] === null ) { |
Index: trunk/extensions/QPoll/ctrl/question/qp_textquestion.php |
— | — | @@ -6,22 +6,41 @@ |
7 | 7 | |
8 | 8 | /** |
9 | 9 | * 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 |
11 | 11 | */ |
12 | 12 | class qp_TextQuestionOptions { |
13 | 13 | |
14 | 14 | # boolean, indicates whether incoming tokens are category list elements |
15 | 15 | var $isCatDef; |
| 16 | + # type of created element (text,radio,checkbox) |
| 17 | + var $type; |
16 | 18 | # 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' |
18 | 21 | var $catDefIdx; |
19 | 22 | # list of input options; array whose every element is a string |
20 | 23 | 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 | + ); |
26 | 45 | # a pointer to last element in $this->input_options array |
27 | 46 | var $iopt_last; |
28 | 47 | |
— | — | @@ -39,10 +58,15 @@ |
40 | 59 | * Applies default settings to the options list |
41 | 60 | * New category begins |
42 | 61 | */ |
43 | | - function startOptionsList() { |
| 62 | + function startOptionsList( $type ) { |
44 | 63 | $this->isCatDef = true; |
| 64 | + $this->type = $type; |
45 | 65 | $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 | + } |
47 | 71 | $this->iopt_last = &$this->input_options[0]; |
48 | 72 | } |
49 | 73 | |
— | — | @@ -51,33 +75,54 @@ |
52 | 76 | * This option will be "current last option" |
53 | 77 | */ |
54 | 78 | 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++; |
60 | 88 | } |
61 | | - $this->catDefIdx++; |
62 | 89 | } |
63 | 90 | |
64 | 91 | /** |
65 | | - * Set string value to current last option |
| 92 | + * Add string part to value of current last option |
66 | 93 | * @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) |
68 | 95 | */ |
69 | 96 | function addToLastOption( $token ) { |
70 | | - # first entry of category options might be definition of |
71 | | - # the current category input textwidth instead |
72 | 97 | $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 | + } |
81 | 124 | } |
| 125 | + # add new input option |
| 126 | + $this->iopt_last .= $token; |
82 | 127 | } |
83 | 128 | |
84 | 129 | /** |
— | — | @@ -92,6 +137,14 @@ |
93 | 138 | # make sure unique elements keys are consequitive starting from 0 |
94 | 139 | $this->input_options[] = $option; |
95 | 140 | } |
| 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 | + } |
96 | 149 | } |
97 | 150 | |
98 | 151 | } /* end of qp_TextQuestionOptions class */ |
— | — | @@ -105,7 +158,8 @@ |
106 | 159 | */ |
107 | 160 | class qp_TextQuestion extends qp_StubQuestion { |
108 | 161 | |
109 | | - const PROP_CAT_PATTERN = '`(<<|>>|{{|}}|\[\[|\]\]|\|)`u'; |
| 162 | + # regexp for separation of proposal line tokens |
| 163 | + static $propCatPattern = null; |
110 | 164 | |
111 | 165 | # $propview is an instance of qp_TextQuestionProposalView |
112 | 166 | # which contains parsed tokens for combined |
— | — | @@ -117,7 +171,48 @@ |
118 | 172 | # only proposal parts and category options |
119 | 173 | var $dbtokens = array(); |
120 | 174 | |
| 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 | + |
121 | 195 | /** |
| 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 | + /** |
122 | 217 | * Parses question body header. |
123 | 218 | * Text questions do not have "body header" (no definitions of spans and categories) |
124 | 219 | * so, this method just splits raw lines of body text to analyze raws in $this->parseBody() |
— | — | @@ -167,11 +262,6 @@ |
168 | 263 | * also may be altered during the poll generation |
169 | 264 | */ |
170 | 265 | function parseBody() { |
171 | | - $matching_braces = array( |
172 | | - '[[' => ']]', |
173 | | - '{{' => '}}', |
174 | | - '<<' => '>>' |
175 | | - ); |
176 | 266 | $proposalId = 0; |
177 | 267 | # Currently, we use just a single instance (no nested categories) |
178 | 268 | $opt = new qp_TextQuestionOptions(); |
— | — | @@ -185,63 +275,70 @@ |
186 | 276 | $this->dbtokens = $brace_stack = array(); |
187 | 277 | $catId = 0; |
188 | 278 | $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 = ''; |
190 | 281 | 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 | + } |
200 | 295 | } |
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; |
220 | 305 | } |
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; |
223 | 327 | } |
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; |
233 | 328 | } |
234 | | - break; |
| 329 | + } catch ( Exception $e ) { |
| 330 | + if ( $e->getMessage() !== 'break' ) { |
| 331 | + throw new MWException( $e->getMessage() ); |
| 332 | + } |
235 | 333 | } |
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 | + } |
238 | 342 | } |
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 | | - } |
246 | 343 | } |
247 | 344 | # check if there is at least one category defined |
248 | 345 | if ( $catId === 0 ) { |
Index: trunk/extensions/QPoll/qp_user.php |
— | — | @@ -444,6 +444,23 @@ |
445 | 445 | return $username; |
446 | 446 | } |
447 | 447 | |
| 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 | + |
448 | 465 | static function onLoadAllMessages() { |
449 | 466 | if ( !self::$messagesLoaded ) { |
450 | 467 | self::$messagesLoaded = true; |
Index: trunk/extensions/QPoll/view/proposal/qp_textquestionproposalview.php |
— | — | @@ -16,15 +16,25 @@ |
17 | 17 | # property 'options' indicates current category options list |
18 | 18 | # property 'error' indicates error message |
19 | 19 | var $viewtokens = array(); |
| 20 | + var $lastTokenType = ''; |
20 | 21 | |
21 | 22 | /** |
22 | 23 | * Add new proposal part (between two categories or line bounds) |
23 | 24 | * It is just an element of string type |
24 | 25 | * |
25 | | - * @param $prop string proposal part |
| 26 | + * @param $token string proposal part |
26 | 27 | */ |
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'; |
29 | 39 | } |
30 | 40 | |
31 | 41 | /** |
— | — | @@ -39,27 +49,26 @@ |
40 | 50 | */ |
41 | 51 | function addCatDef( qp_TextQuestionOptions $opt, $name, $text_answer, $unanswered ) { |
42 | 52 | # $catdef instanceof stdClass properties: |
| 53 | + # property 'type' contains type of current category: 'text', 'checkbox', 'radio' |
43 | 54 | # property 'options' stores an array of user options |
44 | 55 | # Multiple options will be selected from the list |
45 | 56 | # Single option will be displayed as text input |
46 | 57 | # property 'name' contains name of input element |
47 | 58 | # property 'value' contains value previousely chosen |
48 | 59 | # 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 |
51 | 61 | # property 'unanswered' boolean |
52 | 62 | # true - the question was POSTed but category is unanswered |
53 | 63 | # false - the question was not POSTed or category is answered |
54 | | - $catdef = (object) array( |
| 64 | + $this->viewtokens[] = (object) array( |
| 65 | + 'type' => $opt->type, |
55 | 66 | 'options' => $opt->input_options, |
56 | 67 | 'name' => $name, |
57 | 68 | 'value' => $text_answer, |
58 | | - 'unanswered' => $unanswered |
| 69 | + 'unanswered' => $unanswered, |
| 70 | + 'attributes' => $opt->attributes |
59 | 71 | ); |
60 | | - if ( !is_null( $opt->textwidth ) ) { |
61 | | - $catdef->textwidth = $opt->textwidth; |
62 | | - } |
63 | | - $this->viewtokens[] = $catdef; |
| 72 | + $this->lastTokenType = 'category'; |
64 | 73 | } |
65 | 74 | |
66 | 75 | /** |
— | — | @@ -78,6 +87,9 @@ |
79 | 88 | if ( $errmsg !== '' ) { |
80 | 89 | array_unshift( $this->viewtokens, (object) array( 'error'=> $errmsg ) ); |
81 | 90 | } |
| 91 | + if ( count( $this->viewtokens ) < 2 ) { |
| 92 | + $this->lastTokenType = 'errmsg'; |
| 93 | + } |
82 | 94 | } |
83 | 95 | |
84 | 96 | /** |
Index: trunk/extensions/QPoll/view/question/qp_textquestionview.php |
— | — | @@ -38,6 +38,44 @@ |
39 | 39 | } |
40 | 40 | |
41 | 41 | /** |
| 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 | +/** |
42 | 80 | * Stores question proposals views (see qp_textqestion.php) and |
43 | 81 | * allows to modify these for results of quizes at the later stage (see qp_poll.php) |
44 | 82 | * An attempt to make somewhat cleaner question view |
— | — | @@ -45,7 +83,20 @@ |
46 | 84 | */ |
47 | 85 | class qp_TextQuestionView extends qp_StubQuestionView { |
48 | 86 | |
| 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 |
49 | 98 | var $textInputStyle = ''; |
| 99 | + # view row |
| 100 | + var $vr; |
50 | 101 | |
51 | 102 | /** |
52 | 103 | * @param $parser |
— | — | @@ -54,6 +105,7 @@ |
55 | 106 | */ |
56 | 107 | function __construct( &$parser, &$frame, $showResults ) { |
57 | 108 | parent::__construct( $parser, $frame ); |
| 109 | + $this->vr = new qp_TextQuestionViewRow(); |
58 | 110 | /* todo: implement showResults */ |
59 | 111 | } |
60 | 112 | |
— | — | @@ -62,7 +114,10 @@ |
63 | 115 | } |
64 | 116 | |
65 | 117 | 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 | + } |
67 | 122 | if ( $textwidth !== null ) { |
68 | 123 | $textwidth = intval( $textwidth ); |
69 | 124 | if ( $textwidth > 0 ) { |
— | — | @@ -127,8 +182,10 @@ |
128 | 183 | * @return tagarray |
129 | 184 | */ |
130 | 185 | function renderParsedProposal( &$viewtokens ) { |
131 | | - $row = array(); |
| 186 | + $vr = $this->vr; |
| 187 | + $vr->reset(); |
132 | 188 | foreach ( $viewtokens as $elem ) { |
| 189 | + $vr->cell = array(); |
133 | 190 | if ( is_object( $elem ) ) { |
134 | 191 | if ( isset( $elem->options ) ) { |
135 | 192 | $className = 'cat_part'; |
— | — | @@ -138,7 +195,7 @@ |
139 | 196 | if ( isset( $elem->interpError ) ) { |
140 | 197 | $className = 'cat_noanswer'; |
141 | 198 | # create view for proposal/category error message |
142 | | - $row[] = array( |
| 199 | + $vr->cell[] = array( |
143 | 200 | '__tag' => 'span', |
144 | 201 | 'class' => 'proposalerror', |
145 | 202 | $elem->interpError |
— | — | @@ -146,7 +203,7 @@ |
147 | 204 | } |
148 | 205 | # create view for the input options part |
149 | 206 | if ( count( $elem->options ) === 1 ) { |
150 | | - # one option produces html text input |
| 207 | + # one option produces html text / radio / checkbox input |
151 | 208 | $value = $elem->value; |
152 | 209 | # check, whether the definition of category has "pre-filled" value |
153 | 210 | # single, non-unanswered, non-empty option is a pre-filled value |
— | — | @@ -158,19 +215,23 @@ |
159 | 216 | $input = array( |
160 | 217 | '__tag' => 'input', |
161 | 218 | 'class' => $className, |
162 | | - 'type' => 'text', |
| 219 | + 'type' => $elem->type, |
163 | 220 | 'name' => $elem->name, |
164 | 221 | 'value' => qp_Setup::specialchars( $value ) |
165 | 222 | ); |
| 223 | + if ( $elem->attributes['checked'] !== null ) { |
| 224 | + $input['checked'] = 'checked'; |
| 225 | + } |
166 | 226 | if ( $this->textInputStyle != '' ) { |
167 | 227 | # apply poll's textwidth attribute |
168 | 228 | $input['style'] = $this->textInputStyle; |
169 | 229 | } |
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;'; |
173 | 233 | } |
174 | | - $row[] = $input; |
| 234 | + $vr->cell[] = $input; |
| 235 | + $vr->addCell(); |
175 | 236 | continue; |
176 | 237 | } |
177 | 238 | # multiple options produce html select / options |
— | — | @@ -190,15 +251,16 @@ |
191 | 252 | } |
192 | 253 | $html_options[] = $html_option; |
193 | 254 | } |
194 | | - $row[] = array( |
| 255 | + $vr->cell[] = array( |
195 | 256 | '__tag' => 'select', |
196 | 257 | 'class' => $className, |
197 | 258 | 'name' => $elem->name, |
198 | 259 | $html_options |
199 | 260 | ); |
| 261 | + $vr->addCell(); |
200 | 262 | } elseif ( isset( $elem->error ) ) { |
201 | 263 | # create view for proposal/category error message |
202 | | - $row[] = array( |
| 264 | + $vr->error[] = array( |
203 | 265 | '__tag' => 'span', |
204 | 266 | 'class' => 'proposalerror', |
205 | 267 | $elem->error |
— | — | @@ -208,14 +270,21 @@ |
209 | 271 | } |
210 | 272 | } else { |
211 | 273 | # create view for the proposal part |
212 | | - $row[] = array( |
| 274 | + $vr->cell[] = array( |
213 | 275 | '__tag' => 'span', |
214 | 276 | 'class' => 'prop_part', |
215 | 277 | $this->rtp( $elem ) |
216 | 278 | ); |
| 279 | + $vr->addCell(); |
217 | 280 | } |
218 | 281 | } |
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 ); |
220 | 289 | } |
221 | 290 | |
222 | 291 | /** |
— | — | @@ -239,7 +308,11 @@ |
240 | 309 | foreach ( $this->pviews as &$propview ) { |
241 | 310 | $prop = $this->renderParsedProposal( $propview->viewtokens ); |
242 | 311 | $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 | + } |
244 | 317 | } |
245 | 318 | return $questionTable; |
246 | 319 | } |