r104960 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r104959‎ | r104960 | r104961 >
Date:08:31, 2 December 2011
Author:questpc
Status:deferred
Tags:
Comment:
Proposal, category and category span definitions now can be multi-line. Question view error message display fix.
Modified paths:
  • /trunk/extensions/QPoll/clientside/qp_user.css (modified) (history)
  • /trunk/extensions/QPoll/ctrl/poll/qp_poll.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/poll/qp_pollstats.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/qp_propattrs.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_mixedquestion.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_questionstats.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/qp_user.php (modified) (history)
  • /trunk/extensions/QPoll/view/proposal/qp_stubquestionproposalview.php (modified) (history)
  • /trunk/extensions/QPoll/view/proposal/qp_tabularquestionproposalview.php (modified) (history)
  • /trunk/extensions/QPoll/view/proposal/qp_textquestionproposalview.php (modified) (history)
  • /trunk/extensions/QPoll/view/question/qp_stubquestionview.php (modified) (history)
  • /trunk/extensions/QPoll/view/question/qp_tabularquestionview.php (modified) (history)
  • /trunk/extensions/QPoll/view/question/qp_textquestionview.php (modified) (history)

Diff [purge]

Index: trunk/extensions/QPoll/i18n/qp.i18n.php
@@ -102,6 +102,8 @@
103103 'qp_error_no_stats' => 'No statistical data is available, because no one has voted for this poll, yet (address=$1).',
104104 'qp_error_address_in_decl_mode' => 'Cannot get an address of the poll in declaration mode.',
105105 'qp_error_question_not_implemented' => 'Questions of such type are not implemented: $1.',
 106+ 'qp_error_question_empty_body' => 'Question body is empty.',
 107+ 'qp_error_question_no_proposals' => 'Question does not have any proposals defined.',
106108 'qp_error_invalid_question_type' => 'Invalid question type: $1.',
107109 'qp_error_invalid_question_name' => 'Invalid question name: $1.',
108110 'qp_error_type_in_stats_mode' => 'Question type cannot be defined in statistical display mode: $1.',
@@ -126,10 +128,12 @@
127129 'qp_error_too_long_category_options_values' => 'Category options values are too long to be stored in the database.',
128130 'qp_error_too_long_proposal_text' => 'Proposal text is too long to be stored in the database.',
129131 '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.',
 132+ 'qp_error_multiline_proposal_name' => 'Proposal name cannot contain multiple text lines.',
 133+ 'qp_error_numeric_proposal_name' => 'Proposal name cannot be numeric.',
131134 'qp_error_too_few_categories' => 'At least two categories must be defined.',
132135 'qp_error_too_few_spans' => 'Every category group must contain at least two subcategories.',
133136 'qp_error_no_answer' => 'Unanswered proposal.',
 137+ 'qp_error_not_enough_categories_answered' => 'Not enough categories selected.',
134138 'qp_error_unique' => 'Question of type unique() has more proposals than possible answers defined: Impossible to complete.',
135139 'qp_error_no_more_attempts' => 'You have reached maximal number of submitting attempts for this poll.',
136140 'qp_error_no_interpretation' => 'Interpretation script does not exist.',
@@ -234,6 +238,8 @@
235239 'qp_error_dependance_in_stats_mode' => 'Poll "dependance" attribute is meaningless in statistical display mode.',
236240 'qp_error_address_in_decl_mode' => 'Poll "address" attribute is meaningless in poll declaration / voting mode.',
237241 'qp_error_question_not_implemented' => 'Invalid value of qustion xml-like "type" attribute was specified. There is no such type of question. Please read the manual for list of valid question types.',
 242+ 'qp_error_question_empty_body' => 'Question body contains no proposals / categories definition.',
 243+ 'qp_error_question_no_proposals' => 'Question body contains no proposals definition, there is nothing to vote for.',
238244 'qp_error_invalid_question_type' => '{{Identical|Invalid value of qustion xml-like "type" attribute was specified. There is no such type of question. Please read the manual for list of valid question types.}}',
239245 'qp_error_invalid_question_name' => '{{Identical|Invalid value of qustion xml-like "name" attribute was specified. Numeric names are not allowed due to possible index clash with integer question ids. Empty names are not allowed as impossible to reference. Too long names are not allowed, otherwise they will be improperly truncated when stored into DB field.}}',
240246 'qp_error_type_in_stats_mode' => 'Question\'s "type" xml-like attribute is meaningless in statistical display mode.',
@@ -250,8 +256,10 @@
251257 '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.',
252258 '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.",
253259 '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.",
254 - 'qp_error_invalid_proposal_name' => 'Proposal name should not be numeric to avoid possible reference clash with proposal ids, which are integer numbers.',
 260+ 'qp_error_multiline_proposal_name' => 'Proposal name should not contain next characters: line feed and carriage return.',
 261+ 'qp_error_numeric_proposal_name' => 'Proposal name should not be numeric to avoid possible reference clash with proposal ids, which are integer numbers.',
255262 'qp_error_too_few_spans' => 'Every category group should include at least two subcategories',
 263+ 'qp_error_not_enough_categories_answered' => 'Current proposal\'s "catreq" attribute or it\'s inherited value of poll / question "catreq" attribute requires more than one category to be selected in the current proposal.',
256264 '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.',
257265 'qp_error_interpretation_no_return' => 'Interpretation script missed an return statement.',
258266 '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.",
@@ -881,7 +889,7 @@
882890 'qp_error_too_long_category_options_values' => 'Die Werte der Kategorieoptionen sind zu lang, um in der Datenbank gespeichert werden zu können.',
883891 'qp_error_too_long_proposal_text' => 'Der vorgeschlagene Text ist zu lang, um in der Datenbank gespeichert werden zu können.',
884892 'qp_error_too_long_proposal_name' => 'Der vorgeschlagene Name ist zu lang, um in der Datenbank gespeichert werden zu können.',
885 - 'qp_error_invalid_proposal_name' => 'Der vorgeschlagene Name darf nicht ausschließlich aus Zahlen bestehen.',
 893+ 'qp_error_numeric_proposal_name' => 'Der vorgeschlagene Name darf nicht ausschließlich aus Zahlen bestehen.',
886894 'qp_error_too_few_categories' => 'Es müssen mindestens zwei Kategorien festgelegt werden.',
887895 'qp_error_too_few_spans' => 'Jede Kategoriengruppe muss mindestens zwei Unterkategorien enthalten.',
888896 'qp_error_no_answer' => 'Unbeantworteter Vorschlag',
@@ -1204,7 +1212,7 @@
12051213 'qp_error_too_long_category_options_values' => 'Les valeurs des options catégorie sont trop longues pour être stockées dans la base de données.',
12061214 'qp_error_too_long_proposal_text' => 'Le texte de la proposition est trop long pour être enregistré dans la base de données',
12071215 'qp_error_too_long_proposal_name' => 'Le nom de la proposition est trop long pour être stocké dans la base de données.',
1208 - 'qp_error_invalid_proposal_name' => 'Le nom de la proposition ne peut pas être numérique.',
 1216+ 'qp_error_numeric_proposal_name' => 'Le nom de la proposition ne peut pas être numérique.',
12091217 'qp_error_too_few_categories' => 'Au moins deux catégories doivent être définies',
12101218 'qp_error_too_few_spans' => 'Toute classe de catégorie nécessite au moins deux réponses possibles définies',
12111219 'qp_error_no_answer' => 'Proposition sans réponse',
@@ -1356,7 +1364,7 @@
13571365 'qp_error_too_long_category_options_values' => 'Os valores das opcións da categoría son longos de máis para almacenalos na base de datos.',
13581366 'qp_error_too_long_proposal_text' => 'O texto da proposta é longo de máis para almacenalo na base de datos.',
13591367 'qp_error_too_long_proposal_name' => 'O nome da proposta é longo de máis para almacenalo na base de datos.',
1360 - 'qp_error_invalid_proposal_name' => 'O nome da proposta non pode ser numérico.',
 1368+ 'qp_error_numeric_proposal_name' => 'O nome da proposta non pode ser numérico.',
13611369 'qp_error_too_few_categories' => 'Débense definir, polo menos, dúas categorías.',
13621370 'qp_error_too_few_spans' => 'Cada clase de categoría necesita definidas, polo menos, dúas respostas posibles.',
13631371 'qp_error_no_answer' => 'Proposta sen resposta.',
@@ -1755,7 +1763,7 @@
17561764 'qp_error_too_long_category_options_values' => 'Le valores del optiones de categoria es troppo longe pro esser immagazinate in le base de datos.',
17571765 'qp_error_too_long_proposal_text' => 'Le texto del proposition es troppo longe pro poter esser immagazinate in le base de datos',
17581766 'qp_error_too_long_proposal_name' => 'Le nomine del proposition es troppo longe pro poter esser immagazinate in le base de datos.',
1759 - 'qp_error_invalid_proposal_name' => 'Le nomine del proposition non pote esser numeric.',
 1767+ 'qp_error_numeric_proposal_name' => 'Le nomine del proposition non pote esser numeric.',
17601768 'qp_error_too_few_categories' => 'Al minus duo categorias debe esser definite',
17611769 'qp_error_too_few_spans' => 'Cata classe de categoria require le definition de al minus duo responsas possibile',
17621770 'qp_error_no_answer' => 'Proposition sin responsa',
@@ -2146,7 +2154,7 @@
21472155 'qp_error_too_long_category_options_values' => 'Можностите за категоријата се предолги за да можат да се зачуваат во базата.',
21482156 'qp_error_too_long_proposal_text' => 'Текстот на предлогот е предолг за да може да се складира во базата',
21492157 'qp_error_too_long_proposal_name' => 'Името на предлогот е предолго за да се зачува во базата.',
2150 - 'qp_error_invalid_proposal_name' => 'Името на предлогот не може да биде број.',
 2158+ 'qp_error_numeric_proposal_name' => 'Името на предлогот не може да биде број.',
21512159 'qp_error_too_few_categories' => 'Мора да определите барем две категории',
21522160 'qp_error_too_few_spans' => 'Секоја класа на категории бара да определите барем два можни одговора',
21532161 'qp_error_no_answer' => 'Неодговорен предлог',
@@ -2263,7 +2271,7 @@
22642272 'qp_error_too_long_category_options_values' => 'De categorie-optiewaarden zijn te lang om opgeslagen te kunnen worden in de database.',
22652273 'qp_error_too_long_proposal_text' => 'Het tekstvoorstel is te lang om opgeslagen te kunnen worden in de database.',
22662274 'qp_error_too_long_proposal_name' => 'De voorstelnaam is te lang om te worden opgeslagen in de database.',
2267 - 'qp_error_invalid_proposal_name' => 'De naam van het voorstel mag niet numeriek zijn.',
 2275+ 'qp_error_numeric_proposal_name' => 'De naam van het voorstel mag niet numeriek zijn.',
22682276 'qp_error_too_few_categories' => 'Er moeten tenminste twee categorieën gedefinieerd worden.',
22692277 'qp_error_too_few_spans' => 'Voor iedere categorieklasse dienen tenminste twee mogelijk antwoorden gedefinieerd te zijn',
22702278 'qp_error_no_answer' => 'Onbeantwoord voorstel',
@@ -2778,6 +2786,8 @@
27792787 'qp_error_no_stats' => 'Статистика голосования недоступна, так как еще никто не голосовал в этом опросе (address=$1)',
27802788 'qp_error_address_in_decl_mode' => 'Недопустимо задавать адрес опроса (address) в режиме определения',
27812789 'qp_error_question_not_implemented' => 'Вопросы данного типа не реализованы в коде расширения: $1',
 2790+ 'qp_error_question_empty_body' => 'Текст определения вопроса пуст.',
 2791+ 'qp_error_question_no_proposals' => 'Текст определения вопроса не содержит ни одной строки вопроса.',
27822792 'qp_error_invalid_question_type' => 'Недопустимый тип вопроса: $1',
27832793 'qp_error_invalid_question_name' => 'Недопустимое имя вопроса: $1',
27842794 'qp_error_type_in_stats_mode' => 'Недопустимо определять тип вопроса в статистическом режиме: $1',
@@ -2797,10 +2807,12 @@
27982808 'qp_error_too_long_category_option_value' => 'Вариант ответа для данной категории слишком длинный для сохранения в базе данных',
27992809 'qp_error_too_long_category_options_values' => 'Варианты ответов для данной категории слишком длинны для сохранения в базе данных',
28002810 'qp_error_too_long_proposal_text' => 'Строка вопроса слишком длинна для сохранения в базе данных',
2801 - 'qp_error_invalid_proposal_name' => 'Имя строки вопроса не может быть числом.',
 2811+ 'qp_error_multiline_proposal_name' => 'Имя строки вопроса не должно содержать в себе более одной строки текста.',
 2812+ 'qp_error_numeric_proposal_name' => 'Имя строки вопроса не может быть числом.',
28022813 'qp_error_too_few_categories' => 'Каждый вопрос должен иметь по крайней мере два варианта ответа',
28032814 'qp_error_too_few_spans' => 'Каждая подкатегория вопроса требует по меньшей мере два варианта ответа',
28042815 'qp_error_no_answer' => 'Нет ответа на вопрос',
 2816+ 'qp_error_not_enough_categories_answered' => 'Недостаточное количество заполненных категорий в строке вопроса.',
28052817 'qp_error_unique' => 'Опрос, имеющий тип unique(), не должен иметь больше ответов чем вопросов',
28062818 'qp_error_no_more_attempts' => 'Исчерпано количество попыток ответа на данный опрос',
28072819 'qp_error_no_interpretation' => 'Скрипт интерпретации не найден',
Index: trunk/extensions/QPoll/clientside/qp_user.css
@@ -10,7 +10,7 @@
1111 .qpoll table.object .spans { color:Gray; }
1212 .qpoll table.object .categories { color:#444455; }
1313 .qpoll table.object .proposal { }
14 -.qpoll span.proposalerror { color:Red; font-weight:600; }
 14+.qpoll span.proposalerror, div.proposalerror { color:Red; font-weight:600; }
1515 .qpoll tr.proposalerror { background-color: Snow; }
1616 .qpoll .settings td { padding: 0.1em 0.4em 0.1em 0.4em }
1717 .qpoll table.settings { background-color:transparent; }
Index: trunk/extensions/QPoll/ctrl/qp_propattrs.php
@@ -19,7 +19,8 @@
2020 # index of $this->rawkeys (when available)
2121 protected $rawidx;
2222 # code of error after getting attributes
23 - # 0 means there is no error
 23+ # integer 0 means there is no error
 24+ # string key of wiki error message
2425 public $error;
2526 # proposal name (for interpretation scripts);
2627 # '' means there is no name
@@ -47,12 +48,20 @@
4849 */
4950 public function setQuestion( qp_StubQuestion $question ) {
5051 $this->question = $question;
51 - $this->rawkeys = array_keys( $question->raws );
52 - $this->rawidx = 0;
 52+ $this->reset();
5353 }
5454
5555 /**
56 - * Iterates through question raws array.
 56+ * Begin new iteration through raw proposals.
 57+ */
 58+ public function reset() {
 59+ $this->rawkeys = array_keys( $this->question->raws );
 60+ $this->rawidx = $this->question->rawProposalKey;
 61+ }
 62+
 63+ /**
 64+ * Iterates through question raws array, using only raw proposals.
 65+ * Raw categories and category spans (if any) are processed separately.
5766 * @return boolean
5867 * true $this properties are populated
5968 * false there are no more raws available
@@ -90,7 +99,7 @@
91100 $this->cpdef = $proposal_text;
92101 $matches = array();
93102 # try to match the raw proposal name (without specific attributes)
94 - preg_match( '`^:\|\s*(.+?)\s*\|\s*(.+?)\s*$`u', $this->cpdef, $matches );
 103+ preg_match( '/^:\|\s*(.+?)\s*\|\s*(.+?)\s*$/su', $this->cpdef, $matches );
95104 if ( count( $matches ) < 3 ||
96105 ( $this->name = $matches[1] ) === '' ) {
97106 # raw proposal name is not defined or empty
@@ -98,7 +107,7 @@
99108 }
100109 # check, whether raw proposal name will fit into the corresponding DB field
101110 if ( strlen( $this->getAttrDef() ) >= qp_Setup::$field_max_len['proposal_text'] ) {
102 - $this->setError( qp_Setup::ERROR_TOO_LONG_PROPNAME );
 111+ $this->setError( 'qp_error_too_long_proposal_name' );
103112 return;
104113 }
105114 # try to get xml-like attributes;
@@ -114,8 +123,11 @@
115124 $this->emptytext = self::getSaneEmptyText( $paramkeys['emptytext'] );
116125 }
117126 if ( is_numeric( $this->name ) ) {
118 - $this->setError( qp_Setup::ERROR_NUMERIC_PROPNAME );
 127+ $this->setError( 'qp_error_numeric_proposal_name' );
119128 return;
 129+ } elseif ( preg_match( '/$.^/msu', $this->name ) ) {
 130+ $this->setError( 'qp_error_multiline_proposal_name' );
 131+ return;
120132 }
121133 # remove raw proposal name from proposal definition
122134 $this->cpdef = $matches[2];
@@ -194,28 +206,25 @@
195207 /**
196208 * Checks, whether current proposal has not enough of user-answered categories,
197209 * according to current question instance.
198 - * @param $proposal_id integer
199 - * id of existing question's proposal
200 - * @param $prop_cats_count
201 - * integer total amount of categories in current proposal
 210+ * @param $answered_cats_count integer
 211+ * number of user-answered categories in current proposal
 212+ * @param $total_cats_count integer
 213+ * total amount of categories in current proposal
202214 * @return boolean
203215 * true not enough of categories are filled
204216 * false otherwise
205217 */
206 - function hasMissingCategories( $proposal_id, $prop_cats_count ) {
 218+ function hasMissingCategories( $answered_cats_count, $total_cats_count ) {
207219 # How many categories has to be answered,
208220 # all defined in row or the amount specified by "catreq" attribute?
209221 # total amount of categories in current proposal
210 - $countRequired = ($this->catreq === 'all') ? $prop_cats_count : $this->catreq;
211 - if ( $countRequired > $prop_cats_count ) {
 222+ $countRequired = ($this->catreq === 'all') ? $total_cats_count : $this->catreq;
 223+ if ( $countRequired > $total_cats_count ) {
212224 # do not require to fill more categories
213225 # than is available in current proposal row
214 - $countRequired = $prop_cats_count;
 226+ $countRequired = $total_cats_count;
215227 }
216 - $answered_cat_count = array_key_exists( $proposal_id, $this->question->mProposalCategoryId ) ?
217 - count( $this->question->mProposalCategoryId[$proposal_id] ) :
218 - 0;
219 - return $answered_cat_count < $countRequired;
 228+ return $answered_cats_count < $countRequired;
220229 }
221230
222231 /**
Index: trunk/extensions/QPoll/ctrl/poll/qp_pollstats.php
@@ -91,13 +91,15 @@
9292 */
9393 function parseInput( $input ) {
9494 $this->questions = new qp_QuestionCollection();
95 - # question attributes split pattern
96 - $splitPattern = '`\s*{|}\s*\n*`u';
97 - # preg_split counts the matches starting from zero
98 - $unparsedAttributes = preg_split( $splitPattern, $input, -1, PREG_SPLIT_NO_EMPTY );
99 - # we count questions starting from 1
100 - array_unshift( $unparsedAttributes, null );
101 - unset( $unparsedAttributes[0] );
 95+ # match questions in statistical mode
 96+ $unparsedAttributes = array();
 97+ if ( preg_match_all( '/^\s*\{(.*?)\}\s*$/msu', $input, $matches ) ) {
 98+ $unparsedAttributes = $matches[1];
 99+ # increase key values of $unparsedAttributes by one,
 100+ # because questions are numbered from one, not zero
 101+ array_unshift( $unparsedAttributes, null );
 102+ unset( $unparsedAttributes[0] );
 103+ }
102104 # first pass: parse the headers
103105 foreach ( $this->pollStore->Questions as $qdata ) {
104106 $question = new qp_QuestionStats(
@@ -106,11 +108,9 @@
107109 $qdata->type,
108110 $qdata->question_id
109111 );
110 - if ( isset( $unparsedAttributes[$qdata->question_id] ) ) {
111 - $attr_str = $unparsedAttributes[$qdata->question_id];
112 - } else {
113 - $attr_str = '';
114 - }
 112+ $attr_str = isset( $unparsedAttributes[$qdata->question_id] ) ?
 113+ $unparsedAttributes[$qdata->question_id] :
 114+ '';
115115 $paramkeys = array();
116116 $type = $this->getQuestionAttributes( $attr_str, $paramkeys );
117117 $question->applyAttributes( $paramkeys );
Index: trunk/extensions/QPoll/ctrl/poll/qp_poll.php
@@ -42,6 +42,13 @@
4343 */
4444 class qp_Poll extends qp_AbstractPoll {
4545
 46+ # question header match pattern without line begin/end, braces, regexp modifiers
 47+ private static $headerPattern = '[^{\|].*?\|.*?[^}\|]';
 48+ # question header split pattern
 49+ private static $splitPattern;
 50+ # question header match pattern with line begin/end, braces, regexp modifiers
 51+ private static $matchPattern;
 52+
4653 # optional address of the poll which must be answered first
4754 var $dependsOn = '';
4855 # optional template used to interpret user vote in the Special:Pollresults page
@@ -56,6 +63,15 @@
5764
5865 function __construct( array $argv, qp_PollView $view ) {
5966 parent::__construct( $argv, $view );
 67+ # Split header / body
 68+ # Includes surrounding newlines and header's curly braces into match in parentheses
 69+ # because raw question bodies has to be trimmed from single surrounding newlines.
 70+ # Otherwise, these newlines will cause false positive detection of multiline proposals.
 71+ self::$splitPattern = '/((?:$.^|^)\s*{' . self::$headerPattern . '\}\s*$.^)/msu';
 72+ # match header
 73+ # Does not include surrounding newlines and header's curly braces into match in parentheses
 74+ # because we need to parse only the inner part (common question and optional categories).
 75+ self::$matchPattern = '/^\s*{(' . self::$headerPattern . ')\}\s*$/msu';
6076 # dependance attr
6177 if ( array_key_exists( 'dependance', $argv ) ) {
6278 $this->dependsOn = trim( $argv['dependance'] );
@@ -324,26 +340,25 @@
325341 */
326342 function parseQuestionsHeaders( $input ) {
327343 $this->questions = new qp_QuestionCollection();
328 - $splitPattern = '`(^|\n\s*)\n\s*{`u';
329 - $unparsedQuestions = preg_split( $splitPattern, $input, -1, PREG_SPLIT_NO_EMPTY );
330 - $questionPattern = '`(.*?[^|\}])\}[ \t]*(\n(.*)|$)`su';
 344+ $unparsedQuestions = preg_split( self::$splitPattern, $input, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );
331345 # first pass: parse the headers
332 - foreach ( $unparsedQuestions as $unparsedQuestion ) {
333 - # If this "unparsedQuestion" is not a full question,
334 - # we put the text into a buffer to add it at the beginning of the next question.
335 - if ( !empty( $buffer ) ) {
336 - $unparsedQuestion = "$buffer\n\n{" . $unparsedQuestion;
337 - }
338 - if ( preg_match( $questionPattern, $unparsedQuestion, $matches ) ) {
339 - $buffer = "";
340 - $header = isset( $matches[1] ) ? $matches[1] : '';
341 - $body = isset( $matches[3] ) ? $matches[3] : null;
 346+ $header =
 347+ $body = '';
 348+ $matches = array();
 349+ while ( list( $key, $part ) = each( $unparsedQuestions ) ) {
 350+ if ( preg_match( self::$matchPattern, $part, $matches ) ) {
 351+ $body = '';
 352+ $header = $matches[1];
 353+ } else {
 354+ $body = $part;
342355 $question = $this->parseQuestionHeader( $header, $body );
343356 $this->questions->add( $question );
344 - } else {
345 - $buffer = $unparsedQuestion;
346357 }
347358 }
 359+ if ( $body === '' ) {
 360+ $question = $this->parseQuestionHeader( $header, $body );
 361+ $this->questions->add( $question );
 362+ }
348363 }
349364
350365 /**
@@ -380,17 +395,20 @@
381396 */
382397 function parseMainHeader( $header ) {
383398 # split common question and question attributes from the header
384 - @list( $common_question, $attr_str ) = preg_split( '`\n\|([^\|].*)\s*$`u', $header, -1, PREG_SPLIT_DELIM_CAPTURE );
385 -
 399+ $parts = explode( '|', $header );
 400+ # last pipe character separates attributes from common question
 401+ $attr_str = array_pop( $parts );
 402+ # this enables to use tables and templates in common question
 403+ $common_question = implode( '|', $parts );
386404 $error_msg = '';
 405+ # assume that question name does not exists (by default)
 406+ $name = null;
387407 if ( isset( $attr_str ) ) {
388408 $paramkeys = array();
389409 $type = $this->getQuestionAttributes( $attr_str, $paramkeys );
390410 if ( !array_key_exists( $type, qp_Setup::$questionTypes ) ) {
391411 $error_msg = wfMsg( 'qp_error_invalid_question_type', qp_Setup::entities( $type ) );
392412 }
393 - # assume that question name does not exists (by default)
394 - $name = null;
395413 if ( $paramkeys['name'] !== null &&
396414 ( $name = trim( $paramkeys['name'] ) ) === '' ||
397415 is_numeric( $name ) ||
@@ -452,9 +470,13 @@
453471 $question = $this->parseMainHeader( $header );
454472 if ( $question->getState() != 'error' ) {
455473 # load previous user choice, when it's available and DB header is compatible with parsed header
456 - if ( $body === null || !method_exists( $question, 'parseBody' ) ) {
 474+ if ( !method_exists( $question, 'parseBody' ) ) {
457475 $question->setState( 'error', wfMsgHtml( 'qp_error_question_not_implemented', qp_Setup::entities( $question->mType ) ) );
 476+ } elseif ( $body === '' ) {
 477+ $question->setState( 'error', wfMsgHtml( 'qp_error_question_empty_body' ) );
458478 } else {
 479+ # build $question->raws[]
 480+ $question->splitRawProposals( $body );
459481 # parse the categories and spans (metacategories)
460482 $question->parseBodyHeader( $body );
461483 }
@@ -466,40 +488,51 @@
467489 * Populates the question with data and builds question->view
468490 */
469491 function parseQuestionBody( qp_StubQuestion $question ) {
470 - if ( $question->getState() == 'error' ) {
471 - # error occured during the previously performed header parsing, do not process further
472 - $question->view->addHeaderError();
473 - # http get: invalid question syntax, parse errors will cause submit button disabled
474 - $this->pollStore->stateError();
475 - return;
476 - }
477 - # populate $question with raw source values
478 - $question->getQuestionAnswer( $this->pollStore );
479 - # check whether the global showresults level prohibits to show statistical data
480 - # to the users who hasn't voted
481 - if ( qp_Setup::$global_showresults <= 1 && !$question->alreadyVoted ) {
482 - # suppress statistical results when the current user hasn't voted the question
483 - $question->view->showResults = array( 'type' => 0 );
484 - }
485 - # parse the question body
486 - # will populate $question->view which can be modified accodring to quiz results
487 - # warning! parameters are passed only by value, not the reference
488 - $question->parseBody();
489 - if ( $this->mBeingCorrected ) {
490 - if ( $question->getState() == '' ) {
491 - # question is OK, store it into pollStore
492 - $this->pollStore->setQuestion( $question );
 492+ try {
 493+ if ( $question->getState() == 'error' ) {
 494+ throw new Exception( 'qp_error' );
 495+ }
 496+ # populate $question with raw source values
 497+ $question->getQuestionAnswer( $this->pollStore );
 498+ # check whether the global showresults level prohibits to show statistical data
 499+ # to the users who hasn't voted
 500+ if ( qp_Setup::$global_showresults <= 1 && !$question->alreadyVoted ) {
 501+ # suppress statistical results when the current user hasn't voted the question
 502+ $question->view->showResults = array( 'type' => 0 );
 503+ }
 504+ # parse the question body
 505+ # will populate $question->view which can be modified accodring to quiz results
 506+ # warning! parameters are passed only by value, not the reference
 507+ $question->parseBody();
 508+ $question->isEmpty();
 509+ if ( $question->getState() == 'error' ) {
 510+ throw new Exception( 'qp_error' );
 511+ }
 512+ if ( $this->mBeingCorrected ) {
 513+ if ( $question->getState() == '' ) {
 514+ # question is OK, store it into pollStore
 515+ $this->pollStore->setQuestion( $question );
 516+ } else {
 517+ # http post: not every proposals were answered: do not update DB
 518+ $this->pollStore->stateIncomplete();
 519+ }
493520 } else {
494 - # http post: not every proposals were answered: do not update DB
495 - $this->pollStore->stateIncomplete();
 521+ # this is the get, not the post: do not update DB
 522+ if ( $question->getState() == '' ) {
 523+ $this->pollStore->stateIncomplete();
 524+ } else {
 525+ # http get: invalid question syntax, parse errors will cause submit button disabled
 526+ $this->pollStore->stateError();
 527+ }
496528 }
497 - } else {
498 - # this is the get, not the post: do not update DB
499 - if ( $question->getState() == '' ) {
500 - $this->pollStore->stateIncomplete();
501 - } else {
 529+ } catch ( Exception $e ) {
 530+ if ( $e->getMessage() === 'qp_error' ) {
 531+ # error occured during the previously performed header parsing, do not process further
502532 # http get: invalid question syntax, parse errors will cause submit button disabled
503533 $this->pollStore->stateError();
 534+ return;
 535+ } else {
 536+ throw new MWException( $e->getMessage() );
504537 }
505538 }
506539 }
Index: trunk/extensions/QPoll/ctrl/question/qp_textquestion.php
@@ -287,12 +287,10 @@
288288
289289 /**
290290 * Parses question body header.
291 - * Text questions do not have "body header" (no definitions of spans and categories)
292 - * so, this method just splits raw lines of body text to analyze raws in $this->parseBody()
293 - * @param $input - the text of question body
 291+ * Text questions do not have "body header" (no definitions of spans and categories).
294292 */
295 - function parseBodyHeader( $input ) {
296 - $this->raws = preg_split( '`\n`su', $input, -1, PREG_SPLIT_NO_EMPTY );
 293+ function parseBodyHeader() {
 294+ /* noop */
297295 }
298296
299297 /**
@@ -478,10 +476,8 @@
479477 $opt->reset();
480478 $this->propview = new qp_TextQuestionProposalView( $proposalId, $this );
481479 # get proposal name and optional attributes (if any)
482 - if ( $prop_attrs->error === qp_Setup::ERROR_TOO_LONG_PROPNAME ) {
483 - $this->propview->prependErrorToken( wfMsg( 'qp_error_too_long_proposal_name' ), 'error' );
484 - } elseif ( $prop_attrs->error === qp_Setup::ERROR_NUMERIC_PROPNAME ) {
485 - $this->propview->prependErrorToken( wfMsg( 'qp_error_invalid_proposal_name' ), 'error' );
 480+ if ( is_string( $prop_attrs->error ) ) {
 481+ $this->propview->prependErrorToken( wfMsg( $prop_attrs->error, 'error' ) );
486482 }
487483 $this->dbtokens = $brace_stack = array();
488484 $dbtokens_idx = -1;
@@ -575,9 +571,17 @@
576572 $this->propview->catreq = $prop_attrs->catreq;
577573 ## Check for unanswered categories.
578574 if ( $this->poll->mBeingCorrected &&
579 - $prop_attrs->hasMissingCategories( $proposalId, $catId ) ) {
 575+ $prop_attrs->hasMissingCategories(
 576+ $answered_cats_count = $this->getAnsweredCatCount( $proposalId ),
 577+ $catId
 578+ ) ) {
580579 $prev_state = $this->getState();
581 - $this->propview->prependErrorToken( wfMsg( 'qp_error_no_answer' ), 'NA' );
 580+ $this->propview->prependErrorToken(
 581+ ($answered_cats_count > 0) ?
 582+ wfMsg( 'qp_error_not_enough_categories_answered' ) :
 583+ wfMsg( 'qp_error_no_answer' )
 584+ , 'NA'
 585+ );
582586 }
583587 $this->view->addProposal( $proposalId, $this->propview );
584588 $proposalId++;
Index: trunk/extensions/QPoll/ctrl/question/qp_mixedquestion.php
@@ -13,14 +13,15 @@
1414 /**
1515 * Creates question view which should be renreded and
1616 * also may be altered during the poll generation
 17+ * todo: this method is too long, split to smaller parts
1718 */
1819 function parseBody() {
1920 global $wgContLang;
20 - $this->mProposalPattern = '`^';
 21+ $proposalPattern = '/^';
2122 foreach ( $this->mCategories as $catDesc ) {
22 - $this->mProposalPattern .= '(\[\]|\(\)|<>)';
 23+ $proposalPattern .= '(\[\]|\(\)|<>)';
2324 }
24 - $this->mProposalPattern .= '(.*)`u';
 25+ $proposalPattern .= '(.*)/su';
2526 $proposalId = -1;
2627 # set static view state for the future qp_TabularQuestionProposalView instances
2728 qp_TabularQuestionProposalView::applyViewState( $this->view );
@@ -30,7 +31,7 @@
3132 # new proposal view
3233 $pview = new qp_TabularQuestionProposalView( $proposalId + 1, $this );
3334 # get the list of categories ($matches)
34 - if ( preg_match( $this->mProposalPattern, $prop_attrs->cpdef, $matches ) ) {
 35+ if ( preg_match( $proposalPattern, $prop_attrs->cpdef, $matches ) ) {
3536 $prop_attrs->dbText = array_pop( $matches ); // current proposal text
3637 array_shift( $matches ); // remove "at whole" match
3738 $last_matches = $matches;
@@ -47,10 +48,8 @@
4849 $pview->text = $prop_attrs->dbText;
4950 $proposalId++;
5051 # set proposal name (if any)
51 - if ( $prop_attrs->error === qp_Setup::ERROR_TOO_LONG_PROPNAME ) {
52 - $pview->prependErrorMessage( wfMsg( 'qp_error_too_long_proposal_name' ), 'error' );
53 - } elseif ( $prop_attrs->error === qp_Setup::ERROR_NUMERIC_PROPNAME ) {
54 - $pview->prependErrorMessage( wfMsg( 'qp_error_invalid_proposal_name' ), 'error' );
 52+ if ( is_string( $prop_attrs->error ) ) {
 53+ $pview->prependErrorMessage( wfMsg( $prop_attrs->error, 'error' ) );
5554 } elseif ( $prop_attrs->name !== '' ) {
5655 $this->mProposalNames[$proposalId] = $prop_attrs->name;
5756 }
@@ -137,9 +136,17 @@
138137 throw new Exception( 'qp_error' );
139138 }
140139 if ( $this->poll->mBeingCorrected &&
141 - $prop_attrs->hasMissingCategories( $proposalId, count( $this->mCategories ) ) ) {
142 - # the proposal was submitted but has not enough answered categories
143 - $pview->prependErrorMessage( wfMsg( 'qp_error_no_answer' ), 'NA' );
 140+ $prop_attrs->hasMissingCategories(
 141+ $answered_cats_count = $this->getAnsweredCatCount( $proposalId ),
 142+ count( $this->mCategories )
 143+ ) ) {
 144+ # the proposal was submitted but has not enough categories answered
 145+ $pview->prependErrorMessage(
 146+ ($answered_cats_count > 0) ?
 147+ wfMsg( 'qp_error_not_enough_categories_answered' ) :
 148+ wfMsg( 'qp_error_no_answer' )
 149+ , 'NA'
 150+ );
144151 # if there was no previous errors, hightlight the whole row
145152 if ( $this->getState() == '' ) {
146153 throw new Exception( 'qp_error' );
Index: trunk/extensions/QPoll/ctrl/question/qp_tabularquestion.php
@@ -10,6 +10,9 @@
1111 */
1212 class qp_TabularQuestion extends qp_StubQuestion {
1313
 14+ private static $categoryPattern = '/^\s*\|(.*)/su';
 15+ private static $categorySpansPattern = '/^\s*[\|!].*/su';
 16+
1417 ## default proposal attributes
1518 # do not allow empty text fields submission / storage by default
1619 var $mEmptyText = false;
@@ -17,48 +20,30 @@
1821 var $mCatReq = 1;
1922
2023 /**
21 - * Constructor
22 - * @public
23 - * @param $poll object
24 - * an instance of question's parent controller
25 - * @param $view object
26 - * an instance of question view "linked" to this question
27 - * @param $questionId integer
28 - * identifier of the question used to generate input names
29 - * @param $name mixed
30 - * null when question has no name / invalid name
31 - * string valid question name
32 - */
33 - function __construct( qp_AbstractPoll $poll, qp_StubQuestionView $view, $questionId, $name ) {
34 - parent::__construct( $poll, $view, $questionId, $name );
35 - $this->mProposalPattern = '`^[^\|\!].*`u';
36 - $this->mCategoryPattern = '`^\|(\n|[^\|].*\n)`u';
37 - }
38 -
39 - /**
4024 * Builds internal & visual representations of categories and spans according to their
4125 * text definition in the question body
42 - * @param $input - the text of question body
4326 */
44 - function parseBodyHeader( $input ) {
45 - $this->raws = preg_split( '`\n`su', $input, -1, PREG_SPLIT_NO_EMPTY );
46 - $categorySpans = false;
47 - if ( isset( $this->raws[1] ) ) {
48 - $categorySpans = preg_match( $this->mCategoryPattern, $this->raws[1] . "\n", $matches );
49 - }
50 - if ( !$categorySpans && isset( $this->raws[0] ) ) {
51 - preg_match( $this->mCategoryPattern, $this->raws[0] . "\n", $matches );
52 - }
 27+ function parseBodyHeader() {
5328 # parse the header - spans and categories
54 - $catString = isset( $matches[1] ) ? $matches[1] : '';
55 - $catRow = $this->parseCategories( $catString );
56 - if ( $categorySpans ) {
 29+ $matches = array();
 30+ if ( isset( $this->raws[1] ) &&
 31+ preg_match( self::$categorySpansPattern, $this->raws[0] ) &&
 32+ preg_match( self::$categoryPattern, $this->raws[1], $matches ) ) {
 33+ # category spans are found, raw proposals begin at key 2
 34+ $this->rawProposalKey = 2;
 35+ $catRow = $this->parseCategories( $matches[1] );
5736 $spansRow = $this->parseCategorySpans( $this->raws[0] );
5837 # if there are multiple spans, "turn on" borders for span and category cells
5938 if ( count( $this->mCategorySpans ) > 1 ) {
6039 $this->view->categoriesStyle .= 'border:1px solid gray;';
6140 }
6241 $this->view->addSpanRow( $spansRow );
 42+ } else {
 43+ # no category spans, raw proposals begin at key 1
 44+ $this->rawProposalKey = 1;
 45+ $catRow = preg_match( self::$categoryPattern, $this->raws[0], $matches ) ?
 46+ $this->parseCategories( $matches[1] ) :
 47+ $this->parseCategories( $this->raws[0] );
6348 }
6449 # do not render single empty category at all (on user's request)
6550 if ( count( $this->mCategories ) == 1 &&
@@ -78,10 +63,10 @@
7964 # build "raw" $categories array
8065 # split tokens
8166 $cat_split = preg_split( '`({{|}}|\[\[|\]\]|\|)`u', $input, -1, PREG_SPLIT_DELIM_CAPTURE );
82 - $matching_braces = Array();
 67+ $matching_braces = array();
8368 $curr_elem = '';
84 - $categories = Array();
85 - foreach ( $cat_split as $part ) {
 69+ $categories = array();
 70+ foreach ( $cat_split as $key => $part ) {
8671 switch ( $part ) {
8772 case '|' :
8873 if ( count( $matching_braces ) == 0 ) {
@@ -154,9 +139,9 @@
155140 # build "raw" spans array
156141 # split tokens
157142 $span_split = preg_split( '`({{|}}|\[\[|\]\]|\||\!)`u', $input, -1, PREG_SPLIT_DELIM_CAPTURE );
158 - $matching_braces = Array();
 143+ $matching_braces = array();
159144 $curr_elem = null;
160 - $spans = Array();
 145+ $spans = array();
161146 if ( isset( $span_split[0] ) && $span_split[0] == '' ) {
162147 array_shift( $span_split );
163148 if ( isset( $span_split[0] ) && in_array( $span_split[0], array( '!', '|' ) ) ) {
@@ -288,17 +273,12 @@
289274 $prop_attrs = qp_Setup::$propAttrs;
290275 $prop_attrs->setQuestion( $this );
291276 while ( $prop_attrs->iterate() ) {
292 - if ( !preg_match( $this->mProposalPattern, $prop_attrs->cpdef, $matches ) ) {
293 - continue;
294 - }
295277 # new proposal view
296278 $pview = new qp_TabularQuestionProposalView( $proposalId + 1, $this );
297279 $proposalId++;
298 - $prop_attrs->dbText = $pview->text = array_pop( $matches );
299 - if ( $prop_attrs->error === qp_Setup::ERROR_TOO_LONG_PROPNAME ) {
300 - $pview->prependErrorMessage( wfMsg( 'qp_error_too_long_proposal_name' ), 'error' );
301 - } elseif ( $prop_attrs->error === qp_Setup::ERROR_NUMERIC_PROPNAME ) {
302 - $pview->prependErrorMessage( wfMsg( 'qp_error_invalid_proposal_name' ), 'error' );
 280+ $prop_attrs->dbText = $pview->text = $prop_attrs->cpdef;
 281+ if ( is_string( $prop_attrs->error ) ) {
 282+ $pview->prependErrorMessage( wfMsg( $prop_attrs->error ), 'error' );
303283 } elseif ( $prop_attrs->name !== '' ) {
304284 $this->mProposalNames[$proposalId] = $prop_attrs->name;
305285 }
@@ -375,13 +355,21 @@
376356 }
377357 # If the proposal was submitted but unanswered
378358 if ( $this->poll->mBeingCorrected &&
379 - $prop_attrs->hasMissingCategories( $proposalId, count( $this->mCategories ) ) ) {
 359+ $prop_attrs->hasMissingCategories(
 360+ $answered_cats_count = $this->getAnsweredCatCount( $proposalId ),
 361+ count( $this->mCategories )
 362+ ) ) {
380363 # if there was no previous errors, hightlight the whole row
381364 if ( $this->getState() == '' ) {
382365 $pview->addCellsClass( 'error' );
383366 }
384 - # the proposal was submitted but has not enough answered categories
385 - $pview->prependErrorMessage( wfMsg( 'qp_error_no_answer' ), 'NA' );
 367+ # the proposal was submitted but has not enough categories answered
 368+ $pview->prependErrorMessage(
 369+ ($answered_cats_count > 0) ?
 370+ wfMsg( 'qp_error_not_enough_categories_answered' ) :
 371+ wfMsg( 'qp_error_no_answer' )
 372+ , 'NA'
 373+ );
386374 }
387375 if ( $pview->text !== null ) {
388376 $this->view->addProposal( $proposalId, $pview );
Index: trunk/extensions/QPoll/ctrl/question/qp_stubquestion.php
@@ -12,6 +12,11 @@
1313 */
1414 class qp_StubQuestion extends qp_AbstractQuestion {
1515
 16+ # array of raw question source lines
 17+ var $raws;
 18+ # key of first raw proposal in $this->raws
 19+ var $rawProposalKey = 0;
 20+
1621 # optional question literal name, used to address questions in interpretation scripts
1722 var $mName = null;
1823
@@ -70,6 +75,32 @@
7176 return array_search( $proposalName, $this->mProposalNames, true );
7277 }
7378
 79+ private function substBackslashNL( &$s ) {
 80+ # warning: in single quoted regexp replace '\\\' translates into '\';
 81+ # without trim(), regexp match in qp_PropAttrs::getFromSource() will fail.
 82+ $s = preg_replace( '/^(\s*\\\??\s*)$/mu', '', trim( $s ) );
 83+ }
 84+
 85+ /**
 86+ * Split raw question body into raw proposals and optionally
 87+ * raw categories / raw category spans, when available.
 88+ */
 89+ function splitRawProposals( $input ) {
 90+ # detect type of raw proposals
 91+ # originally was: preg_match( '/(?:^|\n)\s*\\??\s*(?:$|\n)/', $input )
 92+ # multiline raw proposals have empty lines and also optionally have lines
 93+ # containing only '\' character.
 94+ if ( preg_match( '/^\s*\\??\s*$/mu', $input ) ) {
 95+ # multiline raw proposals
 96+ # warning: in single quoted regexp split '\\' translates into '\'
 97+ $this->raws = preg_split( '/^(\s*\\??\s*)$/mu', $input, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );
 98+ array_walk( $this->raws, array( __CLASS__, 'substBackslashNL' ) );
 99+ } else {
 100+ # single line raw proposals
 101+ $this->raws = preg_split( '`\n`su', $input, -1, PREG_SPLIT_NO_EMPTY );
 102+ }
 103+ }
 104+
74105 # load some question fields from qp_QuestionData given
75106 # (usually qp_QuestionData is an array property of qp_PollStore instance)
76107 # @param $qdata - an instance of qp_QuestionData
@@ -135,6 +166,19 @@
136167 }
137168
138169 /**
 170+ * @param $proposal_id integer
 171+ * id of existing question's proposal
 172+ * @return integer
 173+ * count of answered categories by current submitting user for
 174+ * specified proposal id
 175+ */
 176+ function getAnsweredCatCount( $proposal_id ) {
 177+ return array_key_exists( $proposal_id, $this->mProposalCategoryId ) ?
 178+ count( $this->mProposalCategoryId[$proposal_id] ) :
 179+ 0;
 180+ }
 181+
 182+ /**
139183 * Applies previousely parsed attributes from main header into question's view
140184 * (all attributes but type)
141185 * @param $paramkeys array
@@ -169,6 +213,15 @@
170214 }
171215
172216 /**
 217+ * Raises an error in case parsed question does not have any proposals.
 218+ */
 219+ function isEmpty() {
 220+ if ( count( $this->mProposalText ) === 0 ) {
 221+ $this->setState( 'error', wfMsgHtml( 'qp_error_question_no_proposals' ) );
 222+ };
 223+ }
 224+
 225+ /**
173226 * Creates question view which should be renreded and
174227 * also may be altered during the poll generation
175228 */
Index: trunk/extensions/QPoll/ctrl/question/qp_questionstats.php
@@ -69,7 +69,6 @@
7070 */
7171 function statsParseBody() {
7272 if ( $this->getState() == 'error' ) {
73 - $this->view->addHeaderError();
7473 return;
7574 }
7675 $catRow = $this->parseCategories();
@@ -82,6 +81,7 @@
8382 $this->view->addSpanRow( $spansRow );
8483 }
8584 $this->view->addCategoryRow( $catRow );
 85+ # set static view state for the future qp_QuestionStatsProposalView instances
8686 qp_QuestionStatsProposalView::applyViewState( $this->view );
8787 foreach ( $this->mProposalText as $proposalId => $text ) {
8888 $pview = new qp_QuestionStatsProposalView( $proposalId, $this );
Index: trunk/extensions/QPoll/qp_user.php
@@ -145,8 +145,6 @@
146146 const NO_ERROR = 0;
147147 const ERROR_MISSED_TITLE = 1;
148148 const ERROR_INVALID_ADDRESS = 2;
149 - const ERROR_TOO_LONG_PROPNAME = 3;
150 - const ERROR_NUMERIC_PROPNAME = 4;
151149
152150 # unicode entity used to display selected checkboxes and radiobuttons in
153151 # result views at Special:Pollresults page
@@ -253,7 +251,7 @@
254252 # otherwise checking of dependance chain will fail:
255253 'dependance' => 768,
256254 # limited due to performance improvements (to fit into DB row),
257 - # and also to properly truncate UFT8 tails:
 255+ # and also to properly truncate UTF-8 tails:
258256 'common_question' => 768,
259257 # limited to maximal length of DB field
260258 'question_name' => 255,
@@ -266,7 +264,7 @@
267265 # may be lost:
268266 'text_answer' => 768,
269267 # limited due to performance improvements (to fit into DB row),
270 - # and also to properly truncate UFT8 tails:
 268+ # and also to properly truncate UTF-8 tails:
271269 'long_interpretation' => 768,
272270 # 'serialized_interpretation' is not longer than DB field size (65535),
273271 # otherwise unserialization of structured answer will be invalid:
@@ -285,16 +283,15 @@
286284 # result views at Special:Pollresults page
287285 static $resultsCheckCode = '+';
288286
289 -
290 - static function entities( $s ) {
 287+ public static function entities( $s ) {
291288 return htmlentities( $s, ENT_QUOTES, 'UTF-8' );
292289 }
293290
294 - static function specialchars( $s ) {
 291+ public static function specialchars( $s ) {
295292 return htmlentities( $s, ENT_QUOTES, 'UTF-8' );
296293 }
297294
298 - static function entity_decode( $s ) {
 295+ public static function entity_decode( $s ) {
299296 return html_entity_decode( $s, ENT_QUOTES, 'UTF-8' );
300297 }
301298
Index: trunk/extensions/QPoll/view/proposal/qp_tabularquestionproposalview.php
@@ -106,7 +106,7 @@
107107 * @param $rowClass - string set rowClass value, boolean false (do not set)
108108 */
109109 function prependErrorMessage( $msg, $state, $rowClass = 'proposalerror' ) {
110 - $this->text = $this->bodyErrorMessage( $msg, $state, $rowClass ) . $this->text;
 110+ $this->text = $this->ctrl->view->bodyErrorMessage( $msg, $state, $rowClass ) . $this->text;
111111 }
112112
113113 /**
@@ -117,7 +117,7 @@
118118 * @param $rowClass - string set rowClass value, boolean false (do not set)
119119 */
120120 function setErrorMessage( $msg, $state, $rowClass = 'proposalerror' ) {
121 - $this->text = $this->bodyErrorMessage( $msg, $state, $rowClass );
 121+ $this->text = $this->ctrl->view->bodyErrorMessage( $msg, $state, $rowClass );
122122 }
123123
124124 /**
@@ -199,7 +199,7 @@
200200 }
201201 # highlight the input
202202 qp_Renderer::addClass( $cat_tag, 'cat_error' );
203 - array_unshift( $cat_tag, $this->bodyErrorMessage( $cat_desc, '', false ) . '<br />' );
 203+ array_unshift( $cat_tag, $this->ctrl->view->bodyErrorMessage( $cat_desc, '', false ) . '<br />' );
204204 }
205205 }
206206 return $foundCats;
Index: trunk/extensions/QPoll/view/proposal/qp_stubquestionproposalview.php
@@ -50,25 +50,4 @@
5151 }
5252 }
5353
54 - /**
55 - * Outputs question body parser error/warning message; also set new controller state
56 - * @param $msg - text of message
57 - * @param $state - set new question controller state
58 - * note that the 'error' state cannot be changed and '' state cannot be set
59 - * @param $rowClass - string set rowClass value, boolean false (do not set)
60 - */
61 - function bodyErrorMessage( $msg, $state, $rowClass = 'proposalerror' ) {
62 - $prev_state = $this->ctrl->getState();
63 - # do not clear previous errors (do not call setState() when $state == '')
64 - if ( $state != '' ) {
65 - $this->ctrl->setState( $state, $msg );
66 - }
67 - if ( is_string( $rowClass ) ) {
68 - $this->rowClass = $rowClass;
69 - }
70 - # will show only the first error, when the state is not clean (not '')
71 - return ( $prev_state == '' ) ? '<span class="proposalerror" title="' . qp_Setup::specialchars( $msg ) . '">???</span> ' : '';
72 - }
73 -
7454 } /* end of qp_StubQuestionProposalView */
75 -
Index: trunk/extensions/QPoll/view/proposal/qp_textquestionproposalview.php
@@ -91,7 +91,7 @@
9292 * @param $rowClass - string set rowClass value, boolean false (do not set)
9393 */
9494 function prependErrorToken( $msg, $state, $rowClass = 'proposalerror' ) {
95 - $errmsg = $this->bodyErrorMessage( $msg, $state, $rowClass );
 95+ $errmsg = $this->ctrl->view->bodyErrorMessage( $msg, $state, $rowClass );
9696 # note: when $state == '' every $errmsg is non-empty;
9797 # when $state == 'error' only the first $errmsg is non-empty;
9898 if ( $errmsg !== '' ) {
@@ -110,7 +110,7 @@
111111 * @param $rowClass - string set rowClass value, boolean false (do not set)
112112 */
113113 function addErrorToken( $msg, $state, $rowClass = 'proposalerror' ) {
114 - $errmsg = $this->bodyErrorMessage( $msg, $state, $rowClass );
 114+ $errmsg = $this->ctrl->view->bodyErrorMessage( $msg, $state, $rowClass );
115115 # note: when $state == '' every $errmsg is non-empty;
116116 # when $state == 'error' only the first $errmsg is non-empty;
117117 if ( $errmsg !== '' ) {
@@ -142,7 +142,7 @@
143143 $cat_desc = wfMsg( 'qp_interpetation_wrong_answer' );
144144 }
145145 # mark the input to highlight it during the rendering
146 - if ( ( $msg = $this->bodyErrorMessage( $cat_desc, '', false ) ) !=='' ) {
 146+ if ( ( $msg = $this->ctrl->view->bodyErrorMessage( $cat_desc, '', false ) ) !=='' ) {
147147 # we call with question state = '', so the returned $msg never should be empty
148148 # unless there was a syntax error, however during the interpretation stage there
149149 # should be no syntax errors, so we can assume that $msg is never equal to ''
Index: trunk/extensions/QPoll/view/question/qp_tabularquestionview.php
@@ -126,7 +126,7 @@
127127 * @return boolean true for valid value, false otherwise
128128 */
129129 function isCSSLengthValid( $width ) {
130 - preg_match( '`^\s*(\d+)(px|em|en|%|)\s*$`', $width, $matches );
 130+ preg_match( '`^\s*(\d+)(px|em|%|)\s*$`', $width, $matches );
131131 return count( $matches > 1 ) && $matches[1] > 0;
132132 }
133133
@@ -266,20 +266,13 @@
267267 function renderTable() {
268268 $questionTable = array();
269269 # add header views to $questionTable
 270+ $rowattrs = array();
270271 foreach ( $this->hviews as $header ) {
271 - $rowattrs = array();
272 - $attribute_maps = array();
273 - if ( is_object( $header ) ) {
274 - $row = &$header->row;
275 - $rowattrs['class'] = $header->className;
276 - $attribute_maps = &$header->attribute_maps;
277 - } else {
278 - $row = &$header;
279 - }
 272+ $rowattrs['class'] = $header->className;
280273 if ( $this->transposed ) {
281 - qp_Renderer::addColumn( $questionTable, $row, $rowattrs, 'th', $attribute_maps );
 274+ qp_Renderer::addColumn( $questionTable, $header->row, $rowattrs, 'th', $header->attribute_maps );
282275 } else {
283 - qp_Renderer::addRow( $questionTable, $row, $rowattrs, 'th', $attribute_maps );
 276+ qp_Renderer::addRow( $questionTable, $header->row, $rowattrs, 'th', $header->attribute_maps );
284277 }
285278 }
286279 # add proposal views to $questionTable
Index: trunk/extensions/QPoll/view/question/qp_stubquestionview.php
@@ -45,8 +45,9 @@
4646 */
4747 class qp_StubQuestionView extends qp_AbstractView {
4848
49 - # error message which occured during the question header parsing that will be output later at rendering stage
50 - var $headerErrorMessage = 'Unknown error';
 49+ # error message which occured during the question header parsing that will be
 50+ # output later at rendering stage, empty string means there is no error
 51+ var $headerErrorMessage = '';
5152
5253 # header views (list of tagarrays)
5354 # tagarray is a primitive view without it's own methods
@@ -86,19 +87,6 @@
8788 }
8889
8990 /**
90 - * @param $tagarray array / string row to add to the question's header
91 - */
92 - function addHeader( $tagarray ) {
93 - $this->hviews[] = $tagarray;
94 - }
95 -
96 - function addHeaderError() {
97 - $this->hviews[] = array(
98 - array( '__tag' => 'td', 'class' => 'proposalerror', $this->headerErrorMessage )
99 - );
100 - }
101 -
102 - /**
10391 * Adds table header row to question's view
10492 * @param $row tagarray representation of row
10593 * @param $className CSS class name of row
@@ -113,6 +101,34 @@
114102 }
115103
116104 /**
 105+ * Outputs question body parser error/warning message.
 106+ * Set new controller state.
 107+ * @param $msg string
 108+ * text of message
 109+ * @param $state string
 110+ * set new question controller state;
 111+ * note that the 'error' state cannot be changed and '' state cannot be set
 112+ * @param $rowClass mixed
 113+ * string set rowClass value
 114+ * boolean false (do not set)
 115+ *
 116+ * note: this method should be invoked directly only for header errors generation;
 117+ * body errors should call wrappers defined in their proposal view classes.
 118+ */
 119+ function bodyErrorMessage( $msg, $state, $rowClass = 'proposalerror' ) {
 120+ $prev_state = $this->ctrl->getState();
 121+ # do not clear previous errors (do not call setState() when $state == '')
 122+ if ( $state != '' ) {
 123+ $this->ctrl->setState( $state, $msg );
 124+ }
 125+ if ( is_string( $rowClass ) ) {
 126+ $this->rowClass = $rowClass;
 127+ }
 128+ # will show only the first error, when the state is not clean (not '')
 129+ return ( $prev_state == '' ) ? '<span class="proposalerror" title="' . qp_Setup::specialchars( $msg ) . '">???</span> ' : '';
 130+ }
 131+
 132+ /**
117133 * Render script-generated interpretation errors, when available (quiz mode)
118134 */
119135 function renderInterpErrors() {
@@ -125,17 +141,10 @@
126142 function renderTable() {
127143 $questionTable = array();
128144 # add header views to $questionTable
 145+ $rowattrs = array();
129146 foreach ( $this->hviews as $header ) {
130 - $rowattrs = array();
131 - $attribute_maps = array();
132 - if ( is_object( $header ) ) {
133 - $row = &$header->row;
134 - $rowattrs['class'] = $header->className;
135 - $attribute_maps = &$header->attribute_maps;
136 - } else {
137 - $row = &$header;
138 - }
139 - qp_Renderer::addRow( $questionTable, $row, $rowattrs, 'th', $attribute_maps );
 147+ $rowattrs['class'] = $header->className;
 148+ qp_Renderer::addRow( $questionTable, $header->row, $rowattrs, 'th', $header->attribute_maps );
140149 }
141150 return $questionTable;
142151 }
@@ -170,7 +179,17 @@
171180 array( '__tag' => 'span', 'class' => 'questionId', 0 => $this->ctrl->usedId )
172181 );
173182 }
174 - $tags[] = array( '__tag' => 'div', 0 => $this->rtp( $this->ctrl->mCommonQuestion ) );
 183+ if ( $this->headerErrorMessage !== '' ) {
 184+ # either fatal or proposal error occured
 185+ $tags[] = array(
 186+ '__tag' => 'div',
 187+ 'class' => ( $this->ctrl->getState() === 'error' ) ? 'fatalerror' : 'proposalerror',
 188+ qp_Setup::specialchars( $this->headerErrorMessage )
 189+ );
 190+ }
 191+ $tags[] = array( '__tag' => 'div', $this->rtp( $this->ctrl->mCommonQuestion ) );
 192+ # class 'question_mod4_[0-3]' is used to prettify question table cells;
 193+ # todo: at some later point, when HTML5/CSS3 will take over, this will not be needed.
175194 $tags = array( '__tag' => 'div', '__end' => "\n", 'class' => 'question question_mod4_' . ( $this->ctrl->usedId % 4 ), $tags );
176195 $tags[] = &$output_table;
177196 return qp_Renderer::renderTagArray( $tags );
Index: trunk/extensions/QPoll/view/question/qp_textquestionview.php
@@ -102,7 +102,9 @@
103103 $value = $elem->options[0];
104104 if ( $tagName === 'textarea' ) {
105105 # oversimplicated regexp, but it's enough for our needs
106 - $value = preg_replace( '/<br[\sA-Z\d="]*\/{0,1}>/i', qp_Setup::TEXTAREA_LINES_SEPARATOR, $value, -1, $lines_count );
 106+ # todo: multiline proposals do not require to use <br> to separate lines
 107+ # of pre-filled text, check out when question type="free" will be implemented.
 108+ $value = preg_replace( '/<br[\sA-Z\d="]*\/?>/i', qp_Setup::TEXTAREA_LINES_SEPARATOR, $value, -1, $lines_count );
107109 $lines_count++;
108110 }
109111 $className .= ' cat_prefilled';

Status & tagging log