r103312 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r103311‎ | r103312 | r103313 >
Date:11:14, 16 November 2011
Author:questpc
Status:deferred (Comments)
Tags:
Comment:
qp_PollStore refactoring: added MySQL master/slave support and memory cache support. Get rid of some extra object references (very old legacy code). Refuse to submit polls which have lengths of proposal names larger than the corresponding DB field length. Minimize global wgvars context usage. Custom character for result checkboxes / radiobuttons. qp_QuestionData refactoring. XLS writer fixes. Interpretation results options value allows possible expansions of options list.
Modified paths:
  • /trunk/extensions/QPoll/ctrl/poll/qp_abstractpoll.php (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/question/qp_abstractquestion.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_mixedquestion.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_stubquestion.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_tabularquestion.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_textquestion.php (modified) (history)
  • /trunk/extensions/QPoll/i18n/qp.i18n.php (modified) (history)
  • /trunk/extensions/QPoll/includes/qp_functionshook.php (modified) (history)
  • /trunk/extensions/QPoll/includes/qp_xlswriter.php (modified) (history)
  • /trunk/extensions/QPoll/interpretation/qp_interpret.php (modified) (history)
  • /trunk/extensions/QPoll/model/cache (added) (history)
  • /trunk/extensions/QPoll/model/cache/qp_categorycache.php (added) (history)
  • /trunk/extensions/QPoll/model/cache/qp_pollcache.php (added) (history)
  • /trunk/extensions/QPoll/model/cache/qp_proposalcache.php (added) (history)
  • /trunk/extensions/QPoll/model/cache/qp_questioncache.php (added) (history)
  • /trunk/extensions/QPoll/model/qp_pollstore.php (modified) (history)
  • /trunk/extensions/QPoll/model/qp_questiondata.php (modified) (history)
  • /trunk/extensions/QPoll/qp_user.php (modified) (history)
  • /trunk/extensions/QPoll/specials/qp_results.php (modified) (history)
  • /trunk/extensions/QPoll/view/poll/qp_pollview.php (modified) (history)
  • /trunk/extensions/QPoll/view/proposal/qp_tabularquestionproposalview.php (modified) (history)
  • /trunk/extensions/QPoll/view/xls/qp_xlspoll.php (modified) (history)
  • /trunk/extensions/QPoll/view/xls/qp_xlstextquestion.php (modified) (history)

Diff [purge]

Index: trunk/extensions/QPoll/i18n/qp.i18n.php
@@ -124,6 +124,7 @@
125125 'qp_error_too_long_category_option_value' => 'Category option value is too long to be stored in the database.',
126126 'qp_error_too_long_category_options_values' => 'Category options values are too long to be stored in the database.',
127127 'qp_error_too_long_proposal_text' => 'Proposal text is too long to be stored in the database.',
 128+ 'qp_error_too_long_proposal_name' => 'Proposal name is too long to be stored in the database.',
128129 'qp_error_too_few_categories' => 'At least two categories must be defined.',
129130 'qp_error_too_few_spans' => 'Every category group must contain at least two subcategories.',
130131 'qp_error_no_answer' => 'Unanswered proposal.',
@@ -238,7 +239,8 @@
239240 'qp_error_too_many_spans' => 'There cannot be more category groups defined than the total count of subcategories.',
240241 'qp_error_too_long_category_option_value' => 'Question type="text" categories with more than one text option to chose are displayed as html select/options list. Submitted (chosen) option value is stored in the database field. If the length of chosen value is too long, the value will be partially lost and select/option will not be properly highlighted. That\'s why the length limit is enforced.',
241242 '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.',
242 - '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. Also, proposal name (which may be used by interpretation script) is stored in the same field, when defined. That's why the length limit is enforced.",
 243+ '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.",
 244+ '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.",
243245 'qp_error_too_few_spans' => 'Every category group should include at least two subcategories',
244246 '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.',
245247 'qp_error_interpretation_no_return' => 'Interpretation script missed an return statement.',
Index: trunk/extensions/QPoll/model/cache/qp_pollcache.php
@@ -0,0 +1,323 @@
 2+<?php
 3+
 4+if ( !defined( 'MEDIAWIKI' ) ) {
 5+ die( "This file is part of the QPoll extension. It is not a valid entry point.\n" );
 6+}
 7+
 8+/**
 9+ * Single row cache for poll descriptions.
 10+ * Also, it's a base class for multi-row caches.
 11+ */
 12+class qp_PollCache {
 13+
 14+ # an instance of qp_PollStore
 15+ static $store;
 16+ # instance of DB_MASTER to cache
 17+ static $db;
 18+
 19+ # DB table name
 20+ protected $tableName = 'qp_poll_desc';
 21+ # DB index for replace
 22+ protected $replaceIndex = array( 'poll', 'article_poll' );
 23+ # DB table fields to select / replace
 24+ protected $fields = array( 'pid', 'order_id', 'dependance', 'interpretation_namespace', 'interpretation_title', 'random_question_count' );
 25+
 26+ # iterable database result object when data was loaded from DBMS
 27+ protected $dbres;
 28+ # single-row replace 2d associative keys array for
 29+ # Database::replace using $this->replaceIndex
 30+ # it should have both $this->fields keys and replace index keys
 31+ # inherited multi-row caches use 3d array with multiple rows instead
 32+ protected $replace;
 33+ # whether new rows should be inserted when there is no row specified by $this->replaceIndex in $this->tableName
 34+ protected $createNewRows = false;
 35+ # final result of load (either from DBMS or from memory cache)
 36+ # stdClass which has $this->fields as keys
 37+ # inherited multi-row caches use array( stdClass, stdClass, ...) instead
 38+ var $rows;
 39+ # single-row 2d numeric keys array for memory cache storage
 40+ # it is more compact than $this->replace,
 41+ # it has only integer keys to minimize memory cache RAM usage
 42+ # inherited multi-row caches use similar 3d array instead
 43+ protected $memc_rows;
 44+
 45+ /**
 46+ * Use store for this class and it's ancestors
 47+ * @param $store an instance of qp_PollStore
 48+ */
 49+ static function setStore( qp_PollStore $store ) {
 50+ self::$store = $store;
 51+ }
 52+
 53+ /**
 54+ * Loads $this->rows either from DB or from memory cache.
 55+ * When memory cache is empty, fills it with data got from DB.
 56+ * @param $db instance of DB_MASTER
 57+ * @param $className name of current class or it's ancestors.
 58+ * It is required because PHP 5.2 has no late static binding.
 59+ * $create boolean true, insert new DB row(s), when DB row(s)
 60+ * with specified $this->replaceIndex key does not exists.
 61+ */
 62+ static function load( $db, $className = __CLASS__, $create = false ) {
 63+ global $wgMemc;
 64+ self::$db = $db;
 65+ # poor man way of emulating late static binding
 66+ $self = new $className();
 67+ if ( !($self instanceof self ) ) {
 68+ throw new MWException( 'className parameter has to be a name of ' . __CLASS__ . ' or it\'s ancestors in ' . __METHOD__ );
 69+ }
 70+ $self->createNewRows = $create;
 71+ # try to get $self->rows from memory cache
 72+ if ( ( $self->rows = $wgMemc->get( $self->getMemcKey() ) ) !== false &&
 73+ ( count( $self->rows ) > 0 || !$self->createNewRows ) ) {
 74+ # memory cache hit
 75+ # zero-rows cache read is considered to be a cache hit only when
 76+ # $self->createNewRows === false; otherwise self::create() will fail
 77+ # after self::load() "probe" for yet non-existing poll.
 78+ $self->numRowsToAssocRows();
 79+ return $self->rows;
 80+ }
 81+ # memory cache miss
 82+ # try to load from DB (get $this->dbres)
 83+ $self->loadFromDB();
 84+ # update self-state from $this->dbres
 85+ $self->rows = array();
 86+ $self->memc_rows = array();
 87+ # _try_ to update self-state from $this->dbres
 88+ $self->updateFromDBres();
 89+ if ( count( $self->rows ) === 0 ) {
 90+ # DB miss
 91+ if ( $self->createNewRows ) {
 92+ $self->insertRows();
 93+ # update self from qp_PollStore properties
 94+ # (prepare for memory cache update)
 95+ $self->updateFromPollStore();
 96+ }
 97+ }
 98+ $self->setMemc();
 99+ # note: no need to perform intval() on the selected fields in caller,
 100+ # it's already been done in $this->convertFromString()
 101+ return $self->rows;
 102+ }
 103+
 104+ /**
 105+ * The same as self::load(), but with $create parameter set to true
 106+ * by default.
 107+ */
 108+ static function create( $db, $className = __CLASS__ ) {
 109+ return self::load( $db, $className, true );
 110+ }
 111+
 112+ /**
 113+ * Stores data from current qp_PollStore instance to memory cache,
 114+ * and optionally to DB.
 115+ * @param $db null - will store only to memory cache (assumes that
 116+ * DB is already in sync with qp_PollStore, thus only
 117+ * the memory cache has to be set);
 118+ * instance of DB_MASTER - will store to DB as well;
 119+ * @param $className1, ... $classNameN - one or more PHP class names
 120+ * that will be instantiated to store their partial data from
 121+ * current qp_PollStore;
 122+ *
 123+ */
 124+ static function store( /* $db, $className1, ... $classNameN */ ) {
 125+ $args = func_get_args();
 126+ self::$db = array_shift( $args );
 127+ foreach ( $args as $className ) {
 128+ $self = new $className();
 129+ if ( !($self instanceof self ) ) {
 130+ throw new MWException( 'className parameter has to be a name of ' . __CLASS__ . ' or it\'s ancestors in ' . __METHOD__ );
 131+ }
 132+ if ( self::$db === null ) {
 133+ ## store only to memory cache, DB is assumed to be already
 134+ ## in sync with self::$store
 135+ # update self from qp_PollStore properties
 136+ # (prepare for memory cache update)
 137+ $self->updateFromPollStore();
 138+ # store $this->memc_rows into memory cache
 139+ $self->setMemc();
 140+ } else {
 141+ # store both to DB and to memory cache
 142+ $self->storePolymorph();
 143+ }
 144+ }
 145+ }
 146+
 147+ /**
 148+ * Convert numeric key array $this->rows to
 149+ * $this->rows stdClass with associative keys
 150+ */
 151+ protected function numRowsToAssocRows() {
 152+ # build DBMS-like object row from array row
 153+ if ( count( $this->rows ) > 0 ) {
 154+ $this->rows = (object) array_combine( $this->fields, $this->rows );
 155+ }
 156+ }
 157+
 158+ /**
 159+ * Get string key associated to replaced DB fields
 160+ */
 161+ protected function getMemcKey() {
 162+ if ( self::$store->mArticleId === null ) {
 163+ throw new MWException( "article_id should be set in " . __METHOD__ );
 164+ }
 165+ if ( self::$store->mPollId === null ) {
 166+ throw new MWException( "poll_id should be set in " . __METHOD__ );
 167+ }
 168+ # pd means poll_desc
 169+ return wfMemcKey( 'qpoll', 'pd', self::$store->mArticleId, self::$store->mPollId );
 170+ }
 171+
 172+ /**
 173+ * Select one or more row(s) from DB to $this->dbres
 174+ */
 175+ protected function loadFromDB() {
 176+ $this->dbres = self::$db->select( $this->tableName,
 177+ $this->fields,
 178+ array( 'article_id' => self::$store->mArticleId, 'poll_id' => self::$store->mPollId ),
 179+ __METHOD__
 180+ );
 181+ }
 182+
 183+ /**
 184+ * Set non-string DB row properties to their original types.
 185+ * Without that, cache hit check will fail: integer value will not match string value
 186+ * from DB. Database class returns string values even for integer fields, however
 187+ * qp_PollStore always uses integers for integer properties.
 188+ */
 189+ protected function convertFromString( $row ) {
 190+ $row->pid = intval( $row->pid );
 191+ $row->order_id = intval( $row->order_id );
 192+ $row->interpretation_namespace = intval( $row->interpretation_namespace );
 193+ $row->random_question_count = intval( $row->random_question_count );
 194+ }
 195+
 196+ /**
 197+ * Makes self-state to be in sync with $this->dbres (DB result).
 198+ * That will allow to synchronize memory cache in the next step.
 199+ */
 200+ protected function updateFromDBres() {
 201+ # Populate $this->rows and $this->memc_rows with DB data
 202+ # from $this->dbres.
 203+ # we cannot use Database::fetchRow() because it will set both
 204+ # numeric and associative keys (x2 fields)
 205+ if ( ( $row = self::$db->fetchObject( $this->dbres ) ) !== false ) {
 206+ $this->convertFromString( $row );
 207+ $this->memc_rows = array_values( (array)$row );
 208+ $this->rows = (object) $row;
 209+ }
 210+ }
 211+
 212+ /**
 213+ * Makes self-state to be in sync with qp_PollStore properties.
 214+ * That will allow to synchronize memory cache in the next step.
 215+ */
 216+ protected function updateFromPollStore() {
 217+ # multi-row ancestors should take in account that $this->memc_rows
 218+ # might be uninitialized when this method was called
 219+ # $this->memc_rows = array();
 220+ # $this->rows = array();
 221+ # update $this->memc_rows from store
 222+ $this->memc_rows = array(
 223+ self::$store->pid,
 224+ self::$store->mOrderId,
 225+ self::$store->dependsOn,
 226+ self::$store->interpNS,
 227+ self::$store->interpDBkey,
 228+ self::$store->randomQuestionCount
 229+ );
 230+ # update $this->rows from store
 231+ $this->rows = (object) array_combine( $this->fields, $this->memc_rows );
 232+ }
 233+
 234+ /**
 235+ * Insert new DB row(s) when row(s) with current $this->replaceIndex is
 236+ * not present in DB.
 237+ */
 238+ protected function insertRows() {
 239+ self::$db->insert( $this->tableName,
 240+ array(
 241+ 'article_id' => self::$store->mArticleId,
 242+ 'poll_id' => self::$store->mPollId,
 243+ 'order_id' => self::$store->mOrderId,
 244+ 'dependance' => self::$store->dependsOn,
 245+ 'interpretation_namespace' => self::$store->interpNS,
 246+ 'interpretation_title' => self::$store->interpDBkey,
 247+ 'random_question_count' => self::$store->randomQuestionCount
 248+ ),
 249+ __METHOD__
 250+ );
 251+ # update current instance of qp_PollStore so it will be in sync with DB state
 252+ self::$store->pid = self::$db->insertId();
 253+ }
 254+
 255+ /**
 256+ * Populates memory cache object with $this->memc_rows
 257+ */
 258+ protected function setMemc() {
 259+ global $wgMemc;
 260+ /*
 261+ if ( count( $this->memc_rows ) > 0 ) {
 262+ $wgMemc->set( $this->getMemcKey(), $this->memc_rows );
 263+ } else {
 264+ $wgMemc->delete( $this->getMemcKey() );
 265+ } */
 266+ # Always store the result, even for empty sets to minimize
 267+ # number of DB queries. Empty sets are possible because
 268+ # poll structures are not stored during page view (GET).
 269+ # They will be properly updated during POST by self::store()
 270+ # Only randomized poll description row is stored during GET,
 271+ # as an exception.
 272+ $wgMemc->set( $this->getMemcKey(), $this->memc_rows );
 273+ }
 274+
 275+ /**
 276+ *
 277+ */
 278+ protected function storePolymorph() {
 279+ global $wgMemc;
 280+ $this->replace = array();
 281+ $this->buildReplaceRows();
 282+ # Update self from qp_PollStore properties
 283+ # (prepare for memory cache update).
 284+ # Otherwise $this->memc_rows will not be in sync
 285+ $this->updateFromPollStore();
 286+ if ( count( $this->replace ) < 1 ) {
 287+ # this cannot happen here; however it can happen in ancestor classes
 288+ throw new Exception( "zero rows replace in " . __METHOD__ );
 289+ }
 290+ $replaceRows = ( $curr_cache_rows = $wgMemc->get( $this->getMemcKey() ) ) === false ||
 291+ serialize( $curr_cache_rows ) !== serialize( $this->memc_rows );
 292+ # replace into DB only when current state does not match memory cache
 293+ if ( $replaceRows ) {
 294+ # update DB
 295+ self::$db->replace( $this->tableName,
 296+ array( $this->replaceIndex ),
 297+ $this->replace,
 298+ __METHOD__
 299+ );
 300+ # update memory cache
 301+ $this->setMemc();
 302+ }
 303+ }
 304+
 305+ /**
 306+ * Initializes DB row(s) for Database::replace() operation into
 307+ * $this->replace
 308+ * Also, should keep $this->memc_rows in sync.
 309+ */
 310+ protected function buildReplaceRows() {
 311+ global $wgContLang;
 312+ $this->replace = array(
 313+ 'pid' => self::$store->pid,
 314+ 'article_id' => self::$store->mArticleId,
 315+ 'poll_id' => self::$store->mPollId,
 316+ 'order_id' => self::$store->mOrderId,
 317+ 'dependance' => self::$store->dependsOn,
 318+ 'interpretation_namespace' => self::$store->interpNS,
 319+ 'interpretation_title' => self::$store->interpDBkey,
 320+ 'random_question_count' => self::$store->randomQuestionCount
 321+ );
 322+ }
 323+
 324+} /* end of qp_PollCache class */
Property changes on: trunk/extensions/QPoll/model/cache/qp_pollcache.php
___________________________________________________________________
Added: svn:eol-style
1325 + native
Index: trunk/extensions/QPoll/model/cache/qp_questioncache.php
@@ -0,0 +1,95 @@
 2+<?php
 3+
 4+if ( !defined( 'MEDIAWIKI' ) ) {
 5+ die( "This file is part of the QPoll extension. It is not a valid entry point.\n" );
 6+}
 7+
 8+/**
 9+ * Muitl-row cache for question descriptions.
 10+ * Also it's a base class for proposal / category multi-row caches.
 11+ */
 12+class qp_QuestionCache extends qp_PollCache {
 13+
 14+ # memory cache key prefix
 15+ protected $keyPrefix = 'qc';
 16+ # DB table name
 17+ protected $tableName = 'qp_question_desc';
 18+ # DB index for replace
 19+ protected $replaceIndex = 'question';
 20+ # DB table fields to select / replace
 21+ protected $fields = array( 'question_id', 'type', 'common_question' );
 22+
 23+ protected function numRowsToAssocRows() {
 24+ # build DBMS-like object rows from array rows
 25+ foreach ( $this->rows as &$row ) {
 26+ $row = (object) array_combine( $this->fields, $row );
 27+ }
 28+ }
 29+
 30+ protected function getMemcKey() {
 31+ return wfMemcKey( 'qpoll', $this->keyPrefix, self::$store->pid );
 32+ }
 33+
 34+ protected function loadFromDB() {
 35+ $this->dbres = self::$db->select( $this->tableName,
 36+ $this->fields,
 37+ array( 'pid' => self::$store->pid ),
 38+ __METHOD__
 39+ );
 40+ }
 41+
 42+ protected function convertFromString( $row ) {
 43+ $row->question_id = intval( $row->question_id );
 44+ }
 45+
 46+ protected function updateFromDBres() {
 47+ # Populate $this->rows and $this->memc_rows with DB data
 48+ # from $this->dbres.
 49+ # we cannot use Database::fetchRow() because it will set both
 50+ # numeric and associative keys (x2 fields)
 51+ while ( ( $row = self::$db->fetchObject( $this->dbres ) ) !== false ) {
 52+ $this->convertFromString( $row );
 53+ $this->memc_rows[] = array_values( (array)$row );
 54+ $this->rows[] = (object) $row;
 55+ }
 56+ }
 57+
 58+ /**
 59+ * Unimplemented, because:
 60+ * 1. self::store( null, ... ) is never called on this or ancestors
 61+ * 2. $this->insertRows() is unimplemented
 62+ * 3. $this->buildReplaceRows() populates $this->memc_rows directly
 63+ * (speed optimization).
 64+ * If anything from the list above will change,
 65+ * this method has to be implemented here and in ancestors as well.
 66+ */
 67+ protected function updateFromPollStore() {
 68+ /* noop */
 69+ }
 70+
 71+ /**
 72+ * Insert operation currently is unneeded for question cache and it's ancestors.
 73+ */
 74+ protected function insertRows() {
 75+ throw new Exception( __METHOD__ . ' is unimplemented (currently is unneeded) ' );
 76+ }
 77+
 78+ protected function buildReplaceRows() {
 79+ global $wgContLang;
 80+ $pid = self::$store->pid;
 81+ foreach ( self::$store->Questions as $qkey => $ques ) {
 82+ $common_question = $wgContLang->truncate( $ques->CommonQuestion, qp_Setup::$field_max_len['common_question'] , '' );
 83+ $this->replace[] = array( 'pid' => $pid, 'question_id' => $qkey, 'type' => $ques->type, 'common_question' => $common_question );
 84+ $ques->question_id = $qkey;
 85+ # instead of calling $this->updateFromPollStore(),
 86+ # we build $this->memc_rows[] right here,
 87+ # to avoid double loop against self::$store->Questions
 88+ $this->memc_rows[] = array(
 89+ $qkey,
 90+ $ques->type,
 91+ $common_question
 92+ );
 93+ }
 94+ }
 95+
 96+} /* end of qp_QuestionCache class */
Property changes on: trunk/extensions/QPoll/model/cache/qp_questioncache.php
___________________________________________________________________
Added: svn:eol-style
197 + native
Index: trunk/extensions/QPoll/model/cache/qp_categorycache.php
@@ -0,0 +1,47 @@
 2+<?php
 3+
 4+if ( !defined( 'MEDIAWIKI' ) ) {
 5+ die( "This file is part of the QPoll extension. It is not a valid entry point.\n" );
 6+}
 7+
 8+/**
 9+ * Muitl-row cache for question categories
 10+ */
 11+class qp_CategoryCache extends qp_QuestionCache {
 12+
 13+ # memory cache key prefix
 14+ protected $keyPrefix = 'cc';
 15+ # DB table name
 16+ protected $tableName = 'qp_question_categories';
 17+ # DB table index for replace
 18+ protected $replaceIndex = 'category';
 19+ # DB table fields to select / replace
 20+ protected $fields = array( 'question_id', 'cat_id', 'cat_name' );
 21+
 22+ protected function convertFromString( $row ) {
 23+ $row->question_id = intval( $row->question_id );
 24+ $row->cat_id = intval( $row->cat_id );
 25+ }
 26+
 27+ protected function buildReplaceRows() {
 28+ global $wgContLang;
 29+ $pid = self::$store->pid;
 30+ foreach ( self::$store->Questions as $qkey => $ques ) {
 31+ $ques->packSpans();
 32+ foreach ( $ques->Categories as $catkey => &$Cat ) {
 33+ $cat_name = $Cat['name'];
 34+ $this->replace[] = array( 'pid' => $pid, 'question_id' => $qkey, 'cat_id' => $catkey, 'cat_name' => $cat_name );
 35+ # instead of calling $this->updateFromPollStore(),
 36+ # we build $this->memc_rows[] right here,
 37+ # to avoid double loop against self::$store->Questions
 38+ $this->memc_rows[] = array(
 39+ $qkey,
 40+ $catkey,
 41+ $cat_name
 42+ );
 43+ }
 44+ $ques->restoreSpans();
 45+ }
 46+ }
 47+
 48+} /* end of qp_CategoryCache class */
Property changes on: trunk/extensions/QPoll/model/cache/qp_categorycache.php
___________________________________________________________________
Added: svn:eol-style
149 + native
Index: trunk/extensions/QPoll/model/cache/qp_proposalcache.php
@@ -0,0 +1,48 @@
 2+<?php
 3+
 4+if ( !defined( 'MEDIAWIKI' ) ) {
 5+ die( "This file is part of the QPoll extension. It is not a valid entry point.\n" );
 6+}
 7+
 8+/**
 9+ * Muitl-row cache for question proposals
 10+ */
 11+class qp_ProposalCache extends qp_QuestionCache {
 12+
 13+ # memory cache key prefix
 14+ protected $keyPrefix = 'pc';
 15+ # DB table name
 16+ protected $tableName = 'qp_question_proposals';
 17+ # DB table index for replace
 18+ protected $replaceIndex = 'proposal';
 19+ # DB table fields to select / replace
 20+ protected $fields = array( 'question_id', 'proposal_id', 'proposal_text' );
 21+
 22+ protected function convertFromString( $row ) {
 23+ $row->question_id = intval( $row->question_id );
 24+ $row->proposal_id = intval( $row->proposal_id );
 25+ }
 26+
 27+ protected function buildReplaceRows() {
 28+ global $wgContLang;
 29+ $pid = self::$store->pid;
 30+ foreach ( self::$store->Questions as $qkey => $ques ) {
 31+ foreach ( $ques->ProposalText as $propkey => $ptext ) {
 32+ if ( isset( $ques->ProposalNames[$propkey] ) ) {
 33+ $ptext = qp_QuestionData::getProposalNamePrefix( $ques->ProposalNames[$propkey] ) . $ptext;
 34+ }
 35+ $ptext = $wgContLang->truncate( $ptext, qp_Setup::$field_max_len['proposal_text'] , '' );
 36+ $this->replace[] = array( 'pid' => $pid, 'question_id' => $qkey, 'proposal_id' => $propkey, 'proposal_text' => $ptext );
 37+ # instead of calling $this->updateFromPollStore(),
 38+ # we build $this->memc_rows[] right here,
 39+ # to avoid double loop against self::$store->Questions
 40+ $this->memc_rows[] = array(
 41+ $qkey,
 42+ $propkey,
 43+ $ptext
 44+ );
 45+ }
 46+ }
 47+ }
 48+
 49+} /* end of qp_ProposalCache class */
Property changes on: trunk/extensions/QPoll/model/cache/qp_proposalcache.php
___________________________________________________________________
Added: svn:eol-style
150 + native
Index: trunk/extensions/QPoll/model/qp_questiondata.php
@@ -5,71 +5,98 @@
66 }
77
88 /**
9 - * Poll's single question data object RAM storage
 9+ * Poll's single question data object RAM storage.
 10+ * Also, converts question properties from / to "packed" DB state.
 11+ * Currently, category spans and proposal names are packed, not having
 12+ * their own separate DB fields.
1013 * ( instances usually have short name qdata )
1114 *
1215 * *** Please do not instantiate directly. ***
13 - * *** use qp_PollStore::newQuestionData() instead ***
 16+ * *** use qp_QuestionData::factory() instead ***
1417 *
1518 */
1619 class qp_QuestionData {
1720
18 - # associated view instance (singleton)
 21+ ## associated view instance (singleton)
1922 static protected $view;
2023
21 - // DB index (with current scheme is non-unique)
 24+ ## DB index (with current scheme is non-unique)
2225 var $question_id = null;
23 - // common properties
 26+ ## common properties
2427 var $type;
2528 var $CommonQuestion;
2629 var $Categories;
2730 var $CategorySpans;
2831 var $ProposalText;
 32+ # since v0.8.0a, proposals may be addressed by their names
 33+ # in the interpretation scripts
2934 var $ProposalNames = array();
3035 var $ProposalCategoryId;
3136 var $ProposalCategoryText;
3237 var $alreadyVoted = false; // whether the selected user already voted this question ?
33 - // statistics storage
 38+ ## statistics storage
 39+ # 3d array with number of choices for each [proposal][category]
3440 var $Votes = null;
 41+ # 3d array with floating point percent values for each [proposal][category],
 42+ # calculated from $this->Votes
3543 var $Percents = null;
3644
3745 /**
3846 * Constructor
39 - * @param $argv associative array, where value of key 'from' defines creation method:
40 - * 'postdata' creates qdata from question instance parsed in tag hook handler;
41 - * 'qid' creates new empty instance to be filled with data loaded from DB;
42 - * another entries of $argv define property names and their values
 47+ * @param $argv mixed
 48+ * array creates new empty instance to be filled with data loaded from DB at later stage;
 49+ * keys of $argv define question data property names;
 50+ * qp_StubQuestion creates qdata from question instance already parsed in tag hook handler;
4351 */
4452 function __construct( $argv ) {
4553 self::$view = new stdClass;
46 - if ( array_key_exists( 'from', $argv ) ) {
47 - switch ( $argv[ 'from' ] ) {
48 - case 'postdata' :
49 - $this->type = $argv[ 'type' ];
50 - $this->CommonQuestion = $argv[ 'common_question' ];
51 - $this->Categories = $argv[ 'categories' ];
52 - $this->CategorySpans = $argv[ 'category_spans' ];
53 - $this->ProposalText = $argv[ 'proposal_text' ];
54 - $this->ProposalNames = $argv[ 'proposal_names' ];
55 - $this->ProposalCategoryId = $argv[ 'proposal_category_id' ];
56 - $this->ProposalCategoryText = $argv[ 'proposal_category_text' ];
57 - return;
58 - case 'qid' :
59 - $this->question_id = $argv[ 'qid' ];
60 - $this->type = $argv[ 'type' ];
61 - $this->CommonQuestion = $argv[ 'common_question' ];
62 - $this->Categories = array();
63 - $this->CategorySpans = array();
64 - $this->ProposalText = array();
65 - $this->ProposalCategoryId = array();
66 - $this->ProposalCategoryText = array();
67 - return;
68 - }
 54+ if ( is_array( $argv ) ) {
 55+ # create the very new question data
 56+ $this->question_id = $argv['qid'];
 57+ $this->type = $argv['type'];
 58+ $this->CommonQuestion = $argv['common_question'];
 59+ $this->Categories = array();
 60+ $this->CategorySpans = array();
 61+ $this->ProposalText = array();
 62+ $this->ProposalNames = array();
 63+ $this->ProposalCategoryId = array();
 64+ $this->ProposalCategoryText = array();
 65+ return;
 66+ } elseif ( $argv instanceof qp_StubQuestion ) {
 67+ # create question data from the already existing question
 68+ $this->question_id = $argv->mQuestionId;
 69+ $this->type = $argv->mType;
 70+ $this->CommonQuestion = $argv->mCommonQuestion;
 71+ $this->Categories = $argv->mCategories;
 72+ $this->CategorySpans = $argv->mCategorySpans;
 73+ $this->ProposalText = $argv->mProposalText;
 74+ $this->ProposalNames = $argv->mProposalNames;
 75+ $this->setQuestionAnswer( $argv );
 76+ return;
6977 }
70 - throw new MWException( "Parameter \$argv['from'] is missing or has unsupported value in " . __METHOD__ );
 78+ throw new MWException( "argv is neither an array nor instance of qp_QuestionData in " . __METHOD__ );
7179 }
7280
7381 /**
 82+ * qp_*QuestionData instantiator (factory).
 83+ * Please use it instead of qp_*QuestionData constructors when
 84+ * creating qdata instances.
 85+ */
 86+ static function factory( $argv ) {
 87+ $type = is_array( $argv ) ? $argv['type'] : $argv->mType;
 88+ switch ( $type ) {
 89+ case 'textQuestion' :
 90+ return new qp_TextQuestionData( $argv );
 91+ case 'singleChoice' :
 92+ case 'multipleChoice' :
 93+ case 'mixedChoice' :
 94+ return new qp_QuestionData( $argv );
 95+ default :
 96+ throw new MWException( 'Unknown type of question ' . qp_Setup::specialchars( $type ) . ' in ' . __METHOD__ );
 97+ }
 98+ }
 99+
 100+ /**
74101 * Get appropriate view for Special:Pollresults
75102 */
76103 function getView() {
@@ -82,6 +109,28 @@
83110 }
84111
85112 /**
 113+ * Check whether the previousely stored poll header is
 114+ * compatible with the one defined on the page.
 115+ *
 116+ * Used to reject previous vote in case the header is incompatble.
 117+ */
 118+ function isCompatible( &$question ) {
 119+ if ( $question->mType != $this->type ) {
 120+ return false;
 121+ }
 122+ if ( count( $question->mCategorySpans ) != count( $this->CategorySpans ) ) {
 123+ return false;
 124+ }
 125+ foreach ( $question->mCategorySpans as $spanidx => &$span ) {
 126+ if ( !isset( $this->CategorySpans[$spanidx] ) ||
 127+ $span['count'] != $this->CategorySpans[$spanidx]['count'] ) {
 128+ return false;
 129+ }
 130+ }
 131+ return true;
 132+ }
 133+
 134+ /**
86135 * Integrate spans into categories
87136 */
88137 function packSpans() {
@@ -124,34 +173,15 @@
125174 }
126175
127176 /**
128 - * Check whether the previousely stored poll header is
129 - * compatible with the one defined on the page.
130 - *
131 - * Used to reject previous vote in case the header is incompatble.
132 - */
133 - function isCompatible( &$question ) {
134 - if ( $question->mType != $this->type ) {
135 - return false;
136 - }
137 - if ( count( $question->mCategorySpans ) != count( $this->CategorySpans ) ) {
138 - return false;
139 - }
140 - foreach ( $question->mCategorySpans as $spanidx => &$span ) {
141 - if ( !isset( $this->CategorySpans[$spanidx] ) ||
142 - $span['count'] != $this->CategorySpans[$spanidx]['count'] ) {
143 - return false;
144 - }
145 - }
146 - return true;
147 - }
148 -
149 - /**
150177 * Split raw proposal text from source page text or from DB
151178 * into name part / text part
152179 *
153180 * @param $proposal_text string raw proposal text
154181 * @modifies $proposal_text string proposal text to display
155 - * @return string proposal name or '' when there is no name
 182+ * @return mixed
 183+ * string proposal name
 184+ * string '' when there is no name
 185+ * boolean false, when the name is too long thus cannot be stored in DB
156186 */
157187 static function splitRawProposal( &$proposal_text ) {
158188 $matches = array();
@@ -159,13 +189,16 @@
160190 preg_match( '`^:\|(.+?)\|\s*(.+?)$`u', $proposal_text, $matches );
161191 if ( count( $matches ) > 2 ) {
162192 if ( ( $prop_name = trim( $matches[1] ) ) !== '' ) {
 193+ if ( strlen( $prop_name ) >= qp_Setup::$field_max_len['proposal_text'] ) {
 194+ return false;
 195+ }
163196 # proposal name must be non-empty
164197 $proposal_text = trim( $matches[2] );
165198 }
166199 }
167200 return $prop_name;
168201 }
169 -
 202+
170203 /**
171204 * Return proposal name prefix to be stored in DB (if any)
172205 */
@@ -173,12 +206,82 @@
174207 return ( $name !== '' ) ? ":|{$name}|" : '';
175208 }
176209
 210+ public function setQuestionAnswer( qp_StubQuestion $question ) {
 211+ $this->ProposalCategoryId = $question->mProposalCategoryId;
 212+ $this->ProposalCategoryText = $question->mProposalCategoryText;
 213+ }
 214+
 215+ /**
 216+ * Set count of votes (user choices) for the selected proposal / category
 217+ * @param $propkey integer proposal id
 218+ * @param $catkey integer category id
 219+ */
 220+ function setVote( $propkey, $catkey, $count ) {
 221+ if ( !is_array( $this->Votes ) ) {
 222+ $this->Votes = array();
 223+ }
 224+ if ( !array_key_exists( $propkey, $this->Votes ) ) {
 225+ $this->Votes[ $propkey ] = array_fill( 0, count( $this->Categories ), 0 );
 226+ }
 227+ $this->Votes[ $propkey ][ $catkey ] = $count;
 228+ }
 229+
 230+ /**
 231+ * Calculates Percents[] properties for specified question from
 232+ * it's Votes[] properties.
 233+ * @param $store
 234+ * instance of qp_PollStore associated with $this qdata
 235+ */
 236+ function calculateQuestionStatistics( qp_PollStore $store ) {
 237+ if ( !isset( $this->Votes ) ) {
 238+ return;
 239+ }
 240+ # $this has votes
 241+ $this->restoreSpans();
 242+ $spansUsed = count( $this->CategorySpans ) > 0 ;
 243+ foreach ( $this->ProposalText as $propkey => $proposal_text ) {
 244+ if ( isset( $this->Votes[ $propkey ] ) ) {
 245+ $votes_row = &$this->Votes[ $propkey ];
 246+ if ( $this->type == "singleChoice" ) {
 247+ if ( $spansUsed ) {
 248+ $row_totals = array_fill( 0, count( $this->CategorySpans ), 0 );
 249+ } else {
 250+ $votes_total = 0;
 251+ }
 252+ foreach ( $this->Categories as $catkey => $cat ) {
 253+ if ( isset( $votes_row[ $catkey ] ) ) {
 254+ if ( $spansUsed ) {
 255+ $row_totals[ intval( $cat[ "spanId" ] ) ] += $votes_row[ $catkey ];
 256+ } else {
 257+ $votes_total += $votes_row[ $catkey ];
 258+ }
 259+ }
 260+ }
 261+ } else {
 262+ $votes_total = $store->totalUsersAnsweredQuestion( $this );
 263+ }
 264+ foreach ( $this->Categories as $catkey => $cat ) {
 265+ $num_of_votes = '';
 266+ if ( isset( $votes_row[ $catkey ] ) ) {
 267+ $num_of_votes = $votes_row[ $catkey ];
 268+ if ( $spansUsed ) {
 269+ if ( isset( $this->Categories[ $catkey ][ "spanId" ] ) ) {
 270+ $votes_total = $row_totals[ intval( $this->Categories[ $catkey ][ "spanId" ] ) ];
 271+ }
 272+ }
 273+ }
 274+ $this->Percents[ $propkey ][ $catkey ] = ( $votes_total > 0 ) ? (float) $num_of_votes / (float) $votes_total : 0.0;
 275+ }
 276+ }
 277+ }
 278+ }
 279+
177280 } /* end of qp_QuestionData class */
178281
179282 /**
180283 *
181284 * *** Please do not instantiate directly. ***
182 - * *** use qp_PollStore::newQuestionData() instead ***
 285+ * *** use qp_QuestionData::factory() instead ***
183286 *
184287 */
185288 class qp_TextQuestionData extends qp_QuestionData {
Index: trunk/extensions/QPoll/model/qp_pollstore.php
@@ -5,29 +5,34 @@
66 }
77
88 /**
9 - * poll storage and retrieval using DB
 9+ * Poll storage and retrieval using DB
1010 * one poll may contain several questions
 11+ *
 12+ * Currently, DB_SLAVE is used for reading user answers (from `qp_question_answers`)
 13+ * and for pager methods (statistical export). Everything else should use DB_MASTER
 14+ * to prevent possible inconsistence due to slave lag.
1115 */
1216 class qp_PollStore {
1317
14 - static $db = null;
15 - # indicates whether random questions must be erased / regenerated when the value of
16 - # 'randomize' attribute is changed from non-zero to zero and back
17 - static $purgeRandomQuestions = false;
18 -
19 - /// DB keys
 18+ ## DB keys
2019 var $pid = null;
2120 var $last_uid = null;
2221
23 - # username is used for caching of setLastUser() method (which now may be called multiple times);
 22+ # username is used for caching $this->setLastUser() method
 23+ # (which now may be called multiple times);
2424 # also used by randomizer
 25+ # For anonymous users it might be different from MediaWiki username,
 26+ # because anonymous users can vote in qpoll.
2527 var $username = '';
2628
2729 /*** common properties ***/
 30+ # article_id of wiki page where the poll is located
2831 var $mArticleId = null;
29 - # unique id of poll, used for addressing, also with 'qp_' prefix as the fragment part of the link
 32+ # unique id (text label) of poll, used for addressing,
 33+ # also with 'qp_' prefix as the fragment part of the http link
 34+ # that leads to the poll definition
3035 var $mPollId = null;
31 - # order of poll on the page
 36+ # order of definition of the poll in the source of the wiki page
3237 var $mOrderId = null;
3338
3439 /*** optional attributes ***/
@@ -39,14 +44,14 @@
4045 # '' indicates that interpretation template does not exists (a poll without quiz)
4146 # null indicates that value is unknown (uninitialized yet)
4247 var $interpDBkey = null;
43 - # interpretation of user answer
 48+ # interpretation of user answer (instance of qp_InterpResult)
4449 var $interpResult;
4550 # 1..n - number of random indexes from poll's header; 0 - poll questions are not randomized
4651 # pollstore loads / saves random indexes for every user only when this property is NOT zero
4752 # which improves performance of non-randomized polls
4853 var $randomQuestionCount = null;
4954
50 - # array of QuestionData instances (data from/to DB)
 55+ # array of qp_QuestionData instances (question data convertation from / to DB)
5156 var $Questions = null;
5257 # array of random indexes of Questions[] array (optional)
5358 var $randomQuestions = false;
@@ -54,6 +59,7 @@
5560 # attempts of voting (passing the quiz). number of resubmits
5661 # note: resubmits are counted for syntax-correct answer (when the vote is stored),
5762 # yet the answer still might be logically incorrect (quiz is not passed / partially passed)
 63+ # that depends on the value $this->interpResult->storeErroneous
5864 var $attempts = 0;
5965
6066 # poll processing state, read with getState()
@@ -75,16 +81,17 @@
7682 # true, after the poll results have been successfully stored to DB
7783 var $voteDone = false;
7884
79 - /* $argv[ 'from' ] indicates type of construction, other elements of $argv vary according to 'from'
80 - */
 85+ /**
 86+ * Poll store multi-purpose constructor.
 87+ * @param $argv['from'] indicates type of construction, other elements of $argv
 88+ * vary according to the value of 'from'
 89+ */
8190 function __construct( $argv = null ) {
82 - global $wgParser;
83 - if ( self::$db == null ) {
84 - self::$db = & wfGetDB( DB_MASTER );
85 - }
8691 $this->interpResult = new qp_InterpResult();
 92+ # set poll store of poll descriptions cache and all it's ancestors
 93+ qp_PollCache::setStore( $this );
8794 if ( is_array( $argv ) && array_key_exists( "from", $argv ) ) {
88 - $this->Questions = Array();
 95+ $this->Questions = array();
8996 $this->mCompletedPostData = 'NA';
9097 $this->pid = null;
9198 $is_post = false;
@@ -92,73 +99,12 @@
93100 case 'poll_post' :
94101 $is_post = true;
95102 case 'poll_get' :
96 - if ( array_key_exists( 'title', $argv ) ) {
97 - $title = $argv[ 'title' ];
98 - } else {
99 - $title = $wgParser->getTitle();
100 - }
101 - $this->mArticleId = $title->getArticleID();
102 - if ( !isset( $argv['poll_id'] ) ) {
103 - throw new MWException( 'Parameter "from" = poll_get / poll_post requires parameter "poll_id" in ' . __METHOD__ );
104 - }
105 - $this->mPollId = $argv[ 'poll_id' ];
106 - if ( array_key_exists( 'order_id', $argv ) ) {
107 - $this->mOrderId = $argv[ 'order_id' ];
108 - }
109 - if ( array_key_exists( 'dependance', $argv ) &&
110 - $argv[ 'dependance' ] !== false ) {
111 - $this->dependsOn = $argv[ 'dependance' ];
112 - }
113 - if ( array_key_exists( 'interpretation', $argv ) ) {
114 - # (0,'') indicates that interpretation template does not exists
115 - $this->interpNS = 0;
116 - $this->interpDBkey = '';
117 - if ( $argv['interpretation'] != '' ) {
118 - $interp = Title::newFromText( $argv['interpretation'], NS_QP_INTERPRETATION );
119 - if ( $interp instanceof Title ) {
120 - $this->interpNS = $interp->getNamespace();
121 - $this->interpDBkey = $interp->getDBkey();
122 - }
123 - }
124 - }
125 - if ( array_key_exists( 'randomQuestionCount', $argv ) ) {
126 - $this->randomQuestionCount = $argv['randomQuestionCount'];
127 - }
128 - # do not load / create the poll when article id is unavailable
129 - # (only during newly created page submission)
130 - if ( $this->mArticleId != 0 ) {
131 - if ( $is_post ) {
132 - $this->setPid();
133 - } else {
134 - $this->loadPid();
135 - if ( is_null( $this->pid ) &&
136 - $this->pollDescIsValid() ) {
137 - # try to create poll description (DB state was incomplete)
138 - # todo: check, whether this is really required for random questions
139 - $this->setPid();
140 - }
141 - }
142 - }
 103+ $this->createFromTagData( $argv, $is_post );
143104 break;
144105 case 'pid' :
145106 if ( array_key_exists( 'pid', $argv ) ) {
146107 $pid = intval( $argv[ 'pid' ] );
147 - $res = self::$db->select( 'qp_poll_desc',
148 - array( 'article_id', 'poll_id', 'order_id', 'dependance', 'interpretation_namespace', 'interpretation_title', 'random_question_count' ),
149 - array( 'pid' => $pid ),
150 - __METHOD__ . ":create from pid" );
151 - $row = self::$db->fetchObject( $res );
152 - if ( $row === false ) {
153 - throw new MWException( 'Attempt to create poll from non-existent poll id in ' . __METHOD__ );
154 - }
155 - $this->pid = $pid;
156 - $this->mArticleId = $row->article_id;
157 - $this->mPollId = $row->poll_id;
158 - $this->mOrderId = $row->order_id;
159 - $this->dependsOn = $row->dependance;
160 - $this->interpNS = $row->interpretation_namespace;
161 - $this->interpDBkey = $row->interpretation_title;
162 - $this->randomQuestionCount = $row->random_question_count;
 108+ $this->createFromPid( $pid );
163109 }
164110 break;
165111 default :
@@ -167,15 +113,111 @@
168114 }
169115 }
170116
171 - // special version of constructor that builds pollstore from the given poll address
172 - // @return instance of qp_PollStore on success, false on error
 117+ /**
 118+ * Creates new poll from data available in qpoll tag attributes.
 119+ * Usually that is HTTP GET / POST operation.
 120+ */
 121+ function createFromTagData( &$argv, $is_post ) {
 122+ global $wgParser;
 123+ if ( array_key_exists( 'title', $argv ) ) {
 124+ $title = $argv[ 'title' ];
 125+ } else {
 126+ $title = $wgParser->getTitle();
 127+ }
 128+ $this->mArticleId = $title->getArticleID();
 129+ if ( !isset( $argv['poll_id'] ) ) {
 130+ throw new MWException( 'Parameter "from" = poll_get / poll_post requires parameter "poll_id" in ' . __METHOD__ );
 131+ }
 132+ $this->mPollId = $argv[ 'poll_id' ];
 133+ if ( array_key_exists( 'order_id', $argv ) ) {
 134+ $this->mOrderId = $argv[ 'order_id' ];
 135+ }
 136+ if ( array_key_exists( 'dependance', $argv ) &&
 137+ $argv[ 'dependance' ] !== false ) {
 138+ $this->dependsOn = $argv[ 'dependance' ];
 139+ }
 140+ if ( array_key_exists( 'interpretation', $argv ) ) {
 141+ # (0,'') indicates that interpretation template does not exists
 142+ $this->interpNS = 0;
 143+ $this->interpDBkey = '';
 144+ if ( $argv['interpretation'] != '' ) {
 145+ $interp = Title::newFromText( $argv['interpretation'], NS_QP_INTERPRETATION );
 146+ if ( $interp instanceof Title ) {
 147+ $this->interpNS = $interp->getNamespace();
 148+ $this->interpDBkey = $interp->getDBkey();
 149+ }
 150+ }
 151+ }
 152+ if ( array_key_exists( 'randomQuestionCount', $argv ) ) {
 153+ $this->randomQuestionCount = $argv['randomQuestionCount'];
 154+ }
 155+ # do not load / create the poll when article id is unavailable
 156+ # (during newly created page submission)
 157+ if ( $this->mArticleId != 0 ) {
 158+ if ( $is_post ) {
 159+ # load or create poll description
 160+ $this->setPid();
 161+ } else {
 162+ # try to load poll description
 163+ $this->loadPid();
 164+ if ( is_null( $this->pid ) &&
 165+ $this->pollDescIsValid() &&
 166+ $this->randomQuestionCount > 0 ) {
 167+ # Randomized polls are required to create their descriptions,
 168+ # because questions random seed is generated at
 169+ # the first user's GET, not at the poll POST.
 170+ $this->setPid();
 171+ }
 172+ }
 173+ }
 174+ }
 175+
 176+ /**
 177+ * Creates new poll store from DB pid specified.
 178+ * That is usual way of loading polls from Special:Pollresults page.
 179+ */
 180+ private function createFromPid( $pid ) {
 181+ # We do not need to cache qp_poll_desc by pid here, because
 182+ # polls are created from pid only in Special:Pollresults page,
 183+ # which usually has low load.
 184+ # However, we have to update poll description cache to keep it's
 185+ # state coherent.
 186+ $db = wfGetDB( DB_MASTER );
 187+ $res = $db->select( 'qp_poll_desc',
 188+ array( 'article_id', 'poll_id', 'order_id', 'dependance', 'interpretation_namespace', 'interpretation_title', 'random_question_count' ),
 189+ array( 'pid' => $pid ),
 190+ __METHOD__ . ":create from pid" );
 191+ $row = $db->fetchObject( $res );
 192+ if ( $row === false ) {
 193+ throw new MWException( 'Attempt to create poll from non-existent pid in ' . __METHOD__ );
 194+ }
 195+ # set the whole set of poll description properties
 196+ # note: it is very important to apply correct type conversation,
 197+ # when populating the data from DB rows
 198+ $this->pid = $pid;
 199+ $this->mArticleId = intval( $row->article_id );
 200+ $this->mPollId = $row->poll_id;
 201+ $this->mOrderId = intval( $row->order_id );
 202+ $this->dependsOn = $row->dependance;
 203+ $this->interpNS = intval( $row->interpretation_namespace );
 204+ $this->interpDBkey = $row->interpretation_title;
 205+ $this->randomQuestionCount = intval( $row->random_question_count );
 206+ # write current poll description properties into memory cache
 207+ qp_PollCache::store( null, 'qp_PollCache' );
 208+ }
 209+
 210+ /**
 211+ * Special version of constructor that builds pollstore from the poll address given.
 212+ * It is used in poll dependance checking, parser functions and in statistical mode.
 213+ * @return instance of qp_PollStore on success, false on error
 214+ */
173215 static function newFromAddr( $pollAddr ) {
174 - # build poll object from given poll address in args[0]
 216+ # build poll object from given poll address
175217 $pollAddr = qp_AbstractPoll::getPrefixedPollAddress( $pollAddr );
176218 if ( is_array( $pollAddr ) ) {
177219 list( $pollTitleStr, $pollId ) = $pollAddr;
178220 $pollTitle = Title::newFromURL( $pollTitleStr );
179 - if ( $pollTitle !== null ) {
 221+ if ( $pollTitle instanceof Title ) {
180222 $pollArticleId = intval( $pollTitle->getArticleID() );
181223 if ( $pollArticleId > 0 ) {
182224 return new qp_PollStore( array(
@@ -194,22 +236,9 @@
195237 }
196238
197239 /**
198 - * qdata instantiator (factory)
199 - * Please use it instead of qdata constructors
 240+ * Get string id of current poll
 241+ * @return string id of current poll
200242 */
201 - static function newQuestionData( $argv ) {
202 - switch ( $argv['type'] ) {
203 - case 'textQuestion' :
204 - return new qp_TextQuestionData( $argv );
205 - case 'singleChoice' :
206 - case 'multipleChoice' :
207 - case 'mixedChoice' :
208 - return new qp_QuestionData( $argv );
209 - default :
210 - throw new MWException( 'Unknown type of question ' . qp_Setup::specialchars( $argv['type'] ) . ' in ' . __METHOD__ );
211 - }
212 - }
213 -
214243 function getPollId() {
215244 return $this->mPollId;
216245 }
@@ -234,7 +263,11 @@
235264 !is_null ( $this->randomQuestionCount );
236265 }
237266
238 - # returns Title object, to get a URI path, use Title::getFullText()/getPrefixedText() on it
 267+ /**
 268+ * Get full title (with fragment part) of the current poll.
 269+ * To get an URI path, use Title::getFullText()/getPrefixedText() on it.
 270+ * @return Title object
 271+ */
239272 function getTitle() {
240273 if ( $this->mArticleId === 0 ) {
241274 throw new MWException( __METHOD__ . ' cannot be called for unsaved new pages' );
@@ -246,10 +279,10 @@
247280 throw new MWException( 'Unknown poll id in ' . __METHOD__ );
248281 }
249282 $res = Title::newFromID( $this->mArticleId );
250 - $res->setFragment( qp_AbstractPoll::s_getPollTitleFragment( $this->mPollId ) );
251283 if ( !( $res instanceof Title ) ) {
252 - throw new MWException( 'Invalid title created in ' . __METHOD__ );
 284+ throw new MWException( 'Cannot create poll title in ' . __METHOD__ );
253285 }
 286+ $res->setFragment( qp_AbstractPoll::s_getPollTitleFragment( $this->mPollId ) );
254287 return $res;
255288 }
256289
@@ -269,10 +302,18 @@
270303 return ( $title instanceof Title ) ? $title : null;
271304 }
272305
273 - // warning: will work only after successful loadUserAlreadyVoted() or loadUserVote()
 306+ /**
 307+ * Checks, whether current $this->last_uid already voted for current poll,
 308+ * or not.
 309+ *
 310+ * Warning: will work only after successful $this->loadUserAlreadyVoted()
 311+ * or $this->loadUserVote()
 312+ *
 313+ * @return boolean true user already voted, false otherwise
 314+ */
274315 function isAlreadyVoted() {
275316 if ( is_array( $this->Questions ) && count( $this->Questions > 0 ) ) {
276 - foreach ( $this->Questions as &$qdata ) {
 317+ foreach ( $this->Questions as $qdata ) {
277318 if ( $qdata->alreadyVoted )
278319 return true;
279320 }
@@ -280,14 +321,22 @@
281322 return false;
282323 }
283324
284 - # checks whether the question with specified id exists in the poll store
285 - # @return boolean, true when the question exists
 325+ /**
 326+ * Checks whether the question with specified id exists in the poll store.
 327+ * @return boolean, true when the question exists, false otherwise
 328+ */
286329 function questionExists( $question_id ) {
287330 return array_key_exists( $question_id, $this->Questions );
288331 }
289332
290 - # load questions for the newly created poll (if the poll was voted at least once)
291 - # @return boolean, true when the questions are available, false otherwise (poll was never voted)
 333+ /**
 334+ * Loads questions for the newly created poll.
 335+ * The questions are available only when the poll was voted at least once.
 336+ * The vote might be an empty submission, usually performed by poll creator
 337+ * (so-called "poll save").
 338+ *
 339+ * @return boolean, true questions are available, false otherwise (poll was never voted)
 340+ */
292341 function loadQuestions() {
293342 $result = false;
294343 $typeFromVer0_5 = array(
@@ -295,25 +344,23 @@
296345 "multipleChoicePoll" => "multipleChoice",
297346 "mixedChoicePoll" => "mixedChoice"
298347 );
 348+ $db = wfGetDB( DB_MASTER );
299349 if ( $this->pid !== null ) {
300 - $res = self::$db->select( 'qp_question_desc',
301 - array( 'question_id', 'type', 'common_question' ),
302 - array( 'pid' => $this->pid ),
303 - __METHOD__ );
304 - if ( self::$db->numRows( $res ) > 0 ) {
 350+ $rows = qp_PollCache::load( $db, 'qp_QuestionCache' );
 351+ if ( count( $rows ) > 0 ) {
305352 $result = true;
306 - while ( $row = self::$db->fetchObject( $res ) ) {
307 - $question_id = intval( $row->question_id );
 353+ foreach ( $rows as $row ) {
 354+ $question_id = $row->question_id;
308355 # convert old (v0.5) question type string to the "new" type string
309356 if ( isset( $typeFromVer0_5[$row->type] ) ) {
310357 $row->type = $typeFromVer0_5[$row->type];
311358 }
312 - # create a qp_QuestionData object from DB fields
313 - $this->Questions[ $question_id ] = self::newQuestionData( array(
314 - 'from' => 'qid',
 359+ # create qp_QuestionData object from DB fields
 360+ $this->Questions[$question_id] = qp_QuestionData::factory( array(
315361 'qid' => $question_id,
316362 'type' => $row->type,
317 - 'common_question' => $row->common_question ) );
 363+ 'common_question' => $row->common_question )
 364+ );
318365 }
319366 $this->getCategories();
320367 $this->getProposalText();
@@ -323,25 +370,74 @@
324371 }
325372
326373 /**
327 - * iterates through the list of users who voted the current poll
328 - * @return mixed false on failure, array of
329 - * ( uid=> ('username'=>username, 'interpretation'=>instanceof qp_InterpResult) ) on success;
330 - * warning: resulting array might be empty;
 374+ * Populates $this->Questions->Categories with data loaded and
 375+ * unpacked from DB
331376 */
 377+ private function getCategories() {
 378+ $db = wfGetDB( DB_MASTER );
 379+ $rows = qp_PollCache::load( $db, 'qp_CategoryCache' );
 380+ foreach ( $rows as $row ) {
 381+ $question_id = $row->question_id;
 382+ $cat_id = $row->cat_id;
 383+ if ( $this->questionExists( $question_id ) ) {
 384+ $qdata = $this->Questions[ $question_id ];
 385+ $qdata->Categories[$cat_id]['name'] = $row->cat_name;
 386+ }
 387+ }
 388+ foreach ( $this->Questions as $qdata ) {
 389+ $qdata->restoreSpans();
 390+ }
 391+ }
 392+
 393+ /**
 394+ * Populates $this->Questions->ProposalText with data loaded and
 395+ * unpacked from DB
 396+ */
 397+ private function getProposalText() {
 398+ $db = wfGetDB( DB_MASTER );
 399+ $rows = qp_PollCache::load( $db, 'qp_ProposalCache' );
 400+ # load proposal text from DB
 401+ foreach ( $rows as $row ) {
 402+ $question_id = $row->question_id;
 403+ $proposal_id = $row->proposal_id;
 404+ if ( $this->questionExists( $question_id ) ) {
 405+ $qdata = $this->Questions[ $question_id ];
 406+ $prop_text = $row->proposal_text;
 407+ $prop_name = qp_QuestionData::splitRawProposal( $prop_text );
 408+ if ( $prop_name !== false && $prop_name !== '' ) {
 409+ $qdata->ProposalNames[$proposal_id] = $prop_name;
 410+ }
 411+ $qdata->ProposalText[$proposal_id] = $prop_text;
 412+ }
 413+ }
 414+ }
 415+
 416+ /**
 417+ * Iterates through the list of users who voted in the current poll
 418+ * @return mixed false on failure,
 419+ * array of ( 'uid'=>
 420+ * array('username'=>string, 'interpretation'=>instanceof qp_InterpResult)
 421+ * ) on success;
 422+ * Warning: resulting array might be empty;
 423+ */
332424 function pollVotersPager( $offset = 0, $limit = 20 ) {
333425 if ( $this->pid === null ) {
334426 return false;
335427 }
336 - $qp_users_polls = self::$db->tableName( 'qp_users_polls' );
337 - $qp_users = self::$db->tableName( 'qp_users' );
338 - $query = "SELECT qup.uid AS uid, name AS username, short_interpretation, long_interpretation, structured_interpretation " .
339 - "FROM $qp_users_polls qup " .
340 - "INNER JOIN $qp_users qu ON qup.uid = qu.uid " .
341 - "WHERE pid = " . intval( $this->pid ) . " " .
342 - "LIMIT " . intval( $offset ) . ", " . intval( $limit );
343 - $res = self::$db->query( $query, __METHOD__ );
 428+ $db = wfGetDB( DB_SLAVE );
 429+ $res = $db->select(
 430+ array( 'qu' => 'qp_users', 'qup' => 'qp_users_polls' ),
 431+ array( 'qup.uid AS uid', 'name AS username',
 432+ 'short_interpretation', 'long_interpretation', 'structured_interpretation' ),
 433+ /* WHERE */ array( 'pid' => $this->pid ),
 434+ __METHOD__,
 435+ array( 'OFFSET' => $offset, 'LIMIT' => $limit ),
 436+ /* JOIN */ array(
 437+ 'qu' => array( 'INNER JOIN', 'qup.uid = qu.uid' )
 438+ )
 439+ );
344440 $result = array();
345 - while ( $row = self::$db->fetchObject( $res ) ) {
 441+ while ( $row = $db->fetchObject( $res ) ) {
346442 $interpResult = new qp_InterpResult();
347443 $interpResult->short = $row->short_interpretation;
348444 $interpResult->long = $row->long_interpretation;
@@ -355,23 +451,29 @@
356452 }
357453
358454 /**
359 - * returns voices of the selected users in the selected question of current poll
360 - * @param $uids array of user id's in DB
361 - * @return mixed array [uid][proposal_id][cat_id]=text_answer on success,
362 - * false on failure
 455+ * Get voices of the selected users in the selected question of current poll
 456+ * @param $uids array of poll user id's in DB
 457+ * @return mixed array [uid][proposal_id][cat_id]=text_answer on success,
 458+ * false on failure
363459 */
364 - function questionVoicesRange( $question_id, array $uids ) {
 460+ function questionVoicesRange( $question_id, $uids ) {
365461 if ( $this->pid === null ) {
366462 return false;
367463 }
368 - $qp_question_answers = self::$db->tableName( 'qp_question_answers' );
369 - $query = "SELECT uid, proposal_id, cat_id, text_answer " .
370 - "FROM $qp_question_answers " .
371 - "WHERE pid = " . intval( $this->pid ) . " AND question_id = " . intval( $question_id ) . " AND uid IN (" . implode( ',', array_map( 'intval', $uids ) ) . ") " .
372 - "ORDER BY uid";
373 - $res = self::$db->query( $query, __METHOD__ );
 464+ $db = wfGetDB( DB_SLAVE );
 465+ $res = $db->select(
 466+ 'qp_question_answers',
 467+ array( 'uid', 'proposal_id', 'cat_id', 'text_answer' ),
 468+ /* WHERE */ array(
 469+ 'pid' => $this->pid,
 470+ 'question_id' => $question_id,
 471+ 'uid' /* IN */ => $uids
 472+ ),
 473+ __METHOD__,
 474+ array( 'ORDER BY' => 'uid' )
 475+ );
374476 $result = array();
375 - while ( $row = self::$db->fetchObject( $res ) ) {
 477+ while ( $row = $db->fetchObject( $res ) ) {
376478 $uid = intval( $row->uid );
377479 if ( !isset( $result[$uid] ) ) {
378480 $result[$uid] = array();
@@ -380,29 +482,46 @@
381483 if ( !isset( $result[$uid][$proposal_id] ) ) {
382484 $result[$uid][$proposal_id] = array();
383485 }
384 - $result[$uid][$proposal_id][intval( $row->cat_id )] = ( ( $row->text_answer == "" ) ? "+" : $row->text_answer );
 486+ $result[$uid][$proposal_id][intval( $row->cat_id )] = ( ( $row->text_answer == '' ) ? qp_Setup::$resultsCheckCode : $row->text_answer );
385487 }
386488 return $result;
387489 }
388490
389 - // checks whether single user already voted the poll's questions
390 - // will be written into self::Questions[]->alreadyVoted
391 - // may be used only after loadQuestions()
392 - // returns true when the user voted to any of the currently defined questions, false otherwise
 491+ /**
 492+ * @return boolean
 493+ * true current poll has at least one question loaded (defined)
 494+ * false otherwise
 495+ */
 496+ function hasQuestions() {
 497+ return
 498+ $this->pid !== null &&
 499+ is_array( $this->Questions ) &&
 500+ count( $this->Questions ) > 0;
 501+ }
 502+
 503+ /**
 504+ * Checks whether single user already voted to the poll's questions.
 505+ * Results will be written into self::Questions[]->alreadyVoted properties.
 506+ * Warning: may be used only after calling $this->loadQuestions()
 507+ * @return boolean:
 508+ * true when the user voted to any of the currently defined questions,
 509+ * false otherwise
 510+ */
393511 function loadUserAlreadyVoted() {
394512 $result = false;
395 - if ( $this->pid === null || $this->last_uid === null ||
396 - !is_array( $this->Questions ) || count( $this->Questions ) == 0 ) {
 513+ if ( !$this->hasQuestions() ||
 514+ $this->last_uid === null ) {
397515 return false;
398516 }
399 - $res = self::$db->select( 'qp_question_answers',
 517+ $db = wfGetDB( DB_SLAVE );
 518+ $res = $db->select( 'qp_question_answers',
400519 array( 'DISTINCT question_id' ),
401520 array( 'pid' => $this->pid, 'uid' => $this->last_uid ),
402521 __METHOD__ . ':load one user poll questions alreadyVoted values' );
403 - if ( self::$db->numRows( $res ) == 0 ) {
 522+ if ( $db->numRows( $res ) == 0 ) {
404523 return false;
405524 }
406 - while ( $row = self::$db->fetchObject( $res ) ) {
 525+ while ( $row = $db->fetchObject( $res ) ) {
407526 $question_id = intval( $row->question_id );
408527 if ( $this->questionExists( $question_id ) ) {
409528 $result = $this->Questions[ $question_id ]->alreadyVoted = true;
@@ -411,28 +530,36 @@
412531 return $result;
413532 }
414533
415 - // load single user vote
416 - // also loads short & long answer interpretation, when available
417 - // will be written into self::Questions[]->ProposalCategoryId,ProposalCategoryText,alreadyVoted
418 - // may be used only after loadQuestions()
419 - // returns true when any of currently defined questions has the votes, false otherwise
 534+ /**
 535+ * Load single user vote.
 536+ * Also loads answer interpretations, when available.
 537+ * Will populate:
 538+ * self::Questions[]->ProposalCategoryId,
 539+ * self::Questions[]->ProposalCategoryText,
 540+ * self::Questions[]->alreadyVoted;
 541+ * Warning: May be used only after calling $this->loadQuestions()
 542+ * @return boolean:
 543+ * true when at least one of currently defined questions were voted,
 544+ * false otherwise
 545+ */
420546 function loadUserVote() {
421547 $result = false;
422 - if ( $this->pid === null || $this->last_uid === null ||
423 - !is_array( $this->Questions ) || count( $this->Questions ) == 0 ) {
 548+ if ( !$this->hasQuestions() ||
 549+ $this->last_uid === null ) {
424550 return false;
425551 }
426 - $res = self::$db->select( 'qp_question_answers',
 552+ $db = wfGetDB( DB_SLAVE );
 553+ $res = $db->select( 'qp_question_answers',
427554 array( 'question_id', 'proposal_id', 'cat_id', 'text_answer' ),
428555 array( 'pid' => $this->pid, 'uid' => $this->last_uid ),
429556 __METHOD__ . ':load one user single poll vote' );
430 - if ( self::$db->numRows( $res ) == 0 ) {
 557+ if ( $db->numRows( $res ) == 0 ) {
431558 return false;
432559 }
433 - while ( $row = self::$db->fetchObject( $res ) ) {
 560+ while ( $row = $db->fetchObject( $res ) ) {
434561 $question_id = intval( $row->question_id );
435562 if ( $this->questionExists( $question_id ) ) {
436 - $qdata = &$this->Questions[ $question_id ];
 563+ $qdata = $this->Questions[$question_id];
437564 $result = $qdata->alreadyVoted = true;
438565 $qdata->ProposalCategoryId[ intval( $row->proposal_id ) ][] = intval( $row->cat_id );
439566 $qdata->ProposalCategoryText[ intval( $row->proposal_id ) ][] = $row->text_answer;
@@ -441,151 +568,73 @@
442569 return $result;
443570 }
444571
445 - // load voting statistics (totals) from DB
446 - // input: $questions_set is optional array of integer question_id values of the current poll
447 - // output: $this->Questions[]Votes[] is set on success
 572+ /**
 573+ * Load voting statistics (totals) from DB.
 574+ * $this->Questions[]Votes[] will be set on success.
 575+ * Votes[] are the numbers of choises for each category.
 576+ * @param $questions_set mixed:
 577+ * array with integer question_id values of the current poll
 578+ * which totals will be loaded;
 579+ * false load totals for all the questions of the current poll
 580+ */
448581 function loadTotals( $questions_set = false ) {
449 - if ( $this->pid !== null &&
450 - is_array( $this->Questions ) && count( $this->Questions > 0 ) ) {
451 - $where = 'pid=' . self::$db->addQuotes( $this->pid );
 582+ $db = wfGetDB( DB_SLAVE );
 583+ if ( $this->hasQuestions() ) {
 584+ $where = array( 'pid' => $this->pid );
452585 if ( is_array( $questions_set ) ) {
453 - $where .= ' AND question_id IN (';
454 - $first_elem = true;
455 - foreach ( $questions_set as &$qid ) {
456 - if ( $first_elem ) {
457 - $first_elem = false;
458 - } else {
459 - $where .= ',';
460 - }
461 - $where .= self::$db->addQuotes( $qid );
462 - }
463 - $where .= ')';
 586+ /* IN */ $where['question_id'] = $questions_set;
464587 }
465 - $res = self::$db->select( 'qp_question_answers',
 588+ $res = $db->select( 'qp_question_answers',
466589 array( 'count(uid)', 'question_id', 'proposal_id', 'cat_id' ),
467590 $where,
468591 __METHOD__ . ':load single poll count of user votes',
469 - array( 'GROUP BY' => 'question_id,proposal_id,cat_id' ) );
470 - while ( $row = self::$db->fetchRow( $res ) ) {
471 - $question_id = intval( $row[ "question_id" ] );
472 - $propkey = intval( $row[ "proposal_id" ] );
473 - $catkey = intval( $row[ "cat_id" ] );
 592+ array( 'GROUP BY' => 'question_id,proposal_id,cat_id' )
 593+ );
 594+ while ( $row = $db->fetchRow( $res ) ) {
 595+ $question_id = intval( $row['question_id'] );
 596+ $propkey = intval( $row['proposal_id'] );
 597+ $catkey = intval( $row['cat_id'] );
474598 if ( $this->questionExists( $question_id ) ) {
475 - $qdata = &$this->Questions[ $question_id ];
476 - if ( !is_array( $qdata->Votes ) ) {
477 - $qdata->Votes = Array();
478 - }
479 - if ( !array_key_exists( $propkey, $qdata->Votes ) ) {
480 - $qdata->Votes[ $propkey ] = array_fill( 0, count( $qdata->Categories ), 0 );
481 - }
482 - $qdata->Votes[ $propkey ][ $catkey ] = intval( $row[ "count(uid)" ] );
 599+ $this->Questions[$question_id]->setVote( $propkey, $catkey, intval( $row['count(uid)'] ) );
483600 }
484601 }
485602 }
486603 }
487604
488 - function totalUsersAnsweredQuestion( &$qdata ) {
 605+ /**
 606+ * @param $qdata qp_QuestionData instance to query
 607+ * @return integer
 608+ * count of users who answered to this question
 609+ */
 610+ function totalUsersAnsweredQuestion( qp_QuestionData $qdata ) {
489611 $result = 0;
 612+ $db = wfGetDB( DB_SLAVE );
490613 if ( $this->pid !== null ) {
491 - $res = self::$db->select( 'qp_question_answers',
 614+ $res = $db->select( 'qp_question_answers',
492615 array( 'count(distinct uid)' ),
493616 array( 'pid' => $this->pid, 'question_id' => $qdata->question_id ),
494617 __METHOD__ );
495 - if ( $row = self::$db->fetchRow( $res ) ) {
 618+ if ( $row = $db->fetchRow( $res ) ) {
496619 $result = intval( $row[ "count(distinct uid)" ] );
497620 }
498621 }
499622 return $result;
500623 }
501624
502 - // try to calculate percents for every question where Votes[] are available
 625+ /**
 626+ * Calculates Percents[] properties for every of $this->Questions where
 627+ * Votes[] properties are available.
 628+ */
503629 function calculateStatistics() {
504 - foreach ( $this->Questions as &$qdata ) {
505 - $this->calculateQuestionStatistics( $qdata );
 630+ foreach ( $this->Questions as $qdata ) {
 631+ $qdata->calculateQuestionStatistics( $this );
506632 }
507633 }
508634
509 - // try to calculate percents for the one question
510 - private function calculateQuestionStatistics( &$qdata ) {
511 - if ( isset( $qdata->Votes ) ) { // is "votable"
512 - $qdata->restoreSpans();
513 - $spansUsed = count( $qdata->CategorySpans ) > 0 ;
514 - foreach ( $qdata->ProposalText as $propkey => $proposal_text ) {
515 - if ( isset( $qdata->Votes[ $propkey ] ) ) {
516 - $votes_row = &$qdata->Votes[ $propkey ];
517 - if ( $qdata->type == "singleChoice" ) {
518 - if ( $spansUsed ) {
519 - $row_totals = array_fill( 0, count( $qdata->CategorySpans ), 0 );
520 - } else {
521 - $votes_total = 0;
522 - }
523 - foreach ( $qdata->Categories as $catkey => $cat ) {
524 - if ( isset( $votes_row[ $catkey ] ) ) {
525 - if ( $spansUsed ) {
526 - $row_totals[ intval( $cat[ "spanId" ] ) ] += $votes_row[ $catkey ];
527 - } else {
528 - $votes_total += $votes_row[ $catkey ];
529 - }
530 - }
531 - }
532 - } else {
533 - $votes_total = $this->totalUsersAnsweredQuestion( $qdata );
534 - }
535 - foreach ( $qdata->Categories as $catkey => $cat ) {
536 - $num_of_votes = '';
537 - if ( isset( $votes_row[ $catkey ] ) ) {
538 - $num_of_votes = $votes_row[ $catkey ];
539 - if ( $spansUsed ) {
540 - if ( isset( $qdata->Categories[ $catkey ][ "spanId" ] ) ) {
541 - $votes_total = $row_totals[ intval( $qdata->Categories[ $catkey ][ "spanId" ] ) ];
542 - }
543 - }
544 - }
545 - $qdata->Percents[ $propkey ][ $catkey ] = ( $votes_total > 0 ) ? (float) $num_of_votes / (float) $votes_total : 0.0;
546 - }
547 - }
548 - }
549 - }
550 - }
551 -
552 - private function getCategories() {
553 - $res = self::$db->select( 'qp_question_categories',
554 - array( 'question_id', 'cat_id', 'cat_name' ),
555 - array( 'pid' => $this->pid ),
556 - __METHOD__ );
557 - while ( $row = self::$db->fetchObject( $res ) ) {
558 - $question_id = intval( $row->question_id );
559 - $cat_id = intval( $row->cat_id );
560 - if ( $this->questionExists( $question_id ) ) {
561 - $qdata = &$this->Questions[ $question_id ];
562 - $qdata->Categories[ $cat_id ][ "name" ] = $row->cat_name;
563 - }
564 - }
565 - foreach ( $this->Questions as &$qdata ) {
566 - $qdata->restoreSpans();
567 - }
568 - }
569 -
570 - private function getProposalText() {
571 - $res = self::$db->select( 'qp_question_proposals',
572 - array( 'question_id', 'proposal_id', 'proposal_text' ),
573 - array( 'pid' => $this->pid ),
574 - __METHOD__ );
575 - # load proposal text from DB
576 - while ( $row = self::$db->fetchObject( $res ) ) {
577 - $question_id = intval( $row->question_id );
578 - $proposal_id = intval( $row->proposal_id );
579 - if ( $this->questionExists( $question_id ) ) {
580 - $qdata = &$this->Questions[ $question_id ];
581 - $prop_text = $row->proposal_text;
582 - if ( ( $prop_name = qp_QuestionData::splitRawProposal( $prop_text ) ) !== '' ) {
583 - $qdata->ProposalNames[$proposal_id] = $prop_name;
584 - }
585 - $qdata->ProposalText[$proposal_id] = $prop_text;
586 - }
587 - }
588 - }
589 -
 635+ /**
 636+ * Get state of current poll. See the description of $this->mCompletedPostData
 637+ * property at the top of the class.
 638+ */
590639 function getState() {
591640 return $this->mCompletedPostData;
592641 }
@@ -600,16 +649,18 @@
601650 $this->mCompletedPostData = 'error';
602651 }
603652
604 - # check whether the poll was successfully submitted
605 - # @return boolean - result of operation
 653+ /**
 654+ * Check whether the poll was successfully submitted.
 655+ * @return boolean result of operation
 656+ */
606657 function stateComplete() {
607 - # completed only when previous state was unavaibale; error state can't be completed
608 - if ( $this->mCompletedPostData == 'NA' && count( $this->Questions ) > 0 ) {
 658+ # completed only when previous state was unavaibale;
 659+ # error state cannot be completed
 660+ if ( $this->mCompletedPostData == 'NA' && $this->hasQuestions() ) {
609661 $this->mCompletedPostData = 'complete';
610662 return true;
611 - } else {
612 - return false;
613663 }
 664+ return false;
614665 }
615666
616667 /**
@@ -624,23 +675,30 @@
625676 }
626677
627678 /**
628 - * Loads $this->randomQuestions from DB for current user
629 - * Will be overriden in memory when number of random questions was changed
 679+ * Loads $this->randomQuestions from DB for current user.
 680+ * Will be overriden in RAM when number of random questions was changed.
630681 */
631682 function loadRandomQuestions() {
632683 if ( $this->mArticleId == 0 ) {
633684 $this->randomQuestions = false;
634685 return;
635686 }
636 - if ( is_null( $this->pid ) ) {
 687+ if ( $this->pid === null ) {
637688 throw new MWException( __METHOD__ . ' cannot be called when pid was not set' );
638689 }
639 - if ( is_null( $this->last_uid ) ) {
 690+ if ( $this->last_uid === null ) {
640691 throw new MWException( __METHOD__ . ' cannot be called when uid was not set' );
641692 }
642 - $res = self::$db->select( 'qp_random_questions', 'question_id', array( 'uid' => $this->last_uid, 'pid' => $this->pid ), __METHOD__ );
 693+ # not using DB_SLAVE here due to possible slave lag
 694+ $db = wfGetDB( DB_MASTER );
 695+ $res = $db->select(
 696+ 'qp_random_questions',
 697+ 'question_id',
 698+ array( 'uid' => $this->last_uid, 'pid' => $this->pid ),
 699+ __METHOD__
 700+ );
643701 $this->randomQuestions = array();
644 - while ( $row = self::$db->fetchObject( $res ) ) {
 702+ while ( $row = $db->fetchObject( $res ) ) {
645703 $this->randomQuestions[] = intval( $row->question_id );
646704 }
647705 if ( count( $this->randomQuestions ) === 0 ) {
@@ -653,42 +711,53 @@
654712 /**
655713 * Stores $this->randomQuestions into DB
656714 * Should be called:
657 - * when user views the page with the poll first time
 715+ * when user views the page which has poll definition first time;
658716 * when number of random questions for poll was changed
659717 */
660718 function setRandomQuestions() {
661719 if ( $this->mArticleId == 0 ) {
662720 return;
663721 }
664 - if ( is_null( $this->pid ) ) {
 722+ if ( $this->pid === null ) {
665723 throw new MWException( __METHOD__ . ' cannot be called when pid was not set' );
666724 }
667 - if ( is_null( $this->last_uid ) ) {
 725+ if ( $this->last_uid === null ) {
668726 throw new MWException( __METHOD__ . ' cannot be called when uid was not set' );
669727 }
 728+ $db = wfGetDB( DB_MASTER );
670729 if ( is_array( $this->randomQuestions ) ) {
671730 $data = array();
672731 foreach ( $this->randomQuestions as $qidx ) {
673732 $data[] = array( 'pid' => $this->pid, 'uid' => $this->last_uid, 'question_id' => $qidx );
674733 }
675 - self::$db->begin();
676 - self::$db->delete( 'qp_random_questions',
 734+ $db->begin();
 735+ $db->delete( 'qp_random_questions',
677736 array( 'pid' => $this->pid, 'uid' => $this->last_uid ),
678 - __METHOD__ );
679 - $res = self::$db->insert( 'qp_random_questions',
 737+ __METHOD__
 738+ );
 739+ $res = $db->insert( 'qp_random_questions',
680740 $data,
681 - __METHOD__ . ':set random questions seed' );
682 - self::$db->commit();
 741+ __METHOD__ . ':set random questions seed'
 742+ );
 743+ $db->commit();
683744 return;
684745 }
685746 # this->randomQuestions === false; this poll is not randomized anymore
686 - self::$db->delete( 'qp_random_questions',
 747+ $db->delete( 'qp_random_questions',
687748 array( 'pid' => $this->pid, 'uid' => $this->last_uid ),
688749 __METHOD__ . ':remove question random seed'
689750 );
690751 }
691752
692 - function setLastUser( $username, $store_new_user_to_db = true ) {
 753+ /**
 754+ * Loads poll user from poll username.
 755+ * Please note that qpoll has different usernames of anonymous users
 756+ * and it's own user id's (uid) not matching to MediaWiki user table,
 757+ * because anonymous users may vote to poll questions.
 758+ * @param $username qpoll username
 759+ * @param $db optional instance of DB_MASTER when transaction is in progress
 760+ */
 761+ function setLastUser( $username, $db = null ) {
693762 if ( $this->pid === null ) {
694763 return;
695764 }
@@ -696,30 +765,27 @@
697766 if ( $this->username === $username ) {
698767 return;
699768 }
700 - $res = self::$db->select( 'qp_users', 'uid', array( 'name' => $username ), __METHOD__ );
701 - $row = self::$db->fetchObject( $res );
 769+ if ( $db === null ) {
 770+ $db = wfGetDB( DB_MASTER );
 771+ }
 772+ $res = $db->select( 'qp_users', 'uid', array( 'name' => $username ), __METHOD__ );
 773+ $row = $db->fetchObject( $res );
702774 if ( $row === false ) {
703 - if ( $store_new_user_to_db ) {
704 - self::$db->insert( 'qp_users', array( 'name' => $username ), __METHOD__ . ':UpdateUser' );
705 - $this->last_uid = intval( self::$db->insertId() );
706 - # set username, user was created
707 - $this->username = $username;
708 - } else {
709 - $this->last_uid = null;
710 - return;
711 - }
712 - } else {
713 - $this->last_uid = intval( $row->uid );
714 - # set username, used was loaded
715 - $this->username = $username;
 775+ # there is no such user
 776+ $this->last_uid = null;
 777+ return;
716778 }
717 - $res = self::$db->select( 'qp_users_polls',
 779+ $this->last_uid = intval( $row->uid );
 780+ # set username, user was loaded
 781+ $this->username = $username;
 782+ $res = $db->select( 'qp_users_polls',
718783 array( 'attempts', 'short_interpretation', 'long_interpretation', 'structured_interpretation' ),
719784 array( 'pid' => $this->pid, 'uid' => $this->last_uid ),
720 - __METHOD__ . ':load short & long answer interpretation' );
721 - if ( self::$db->numRows( $res ) != 0 ) {
722 - $row = self::$db->fetchObject( $res );
723 - $this->attempts = $row->attempts;
 785+ __METHOD__ . ':load answer interpretations'
 786+ );
 787+ if ( $db->numRows( $res ) != 0 ) {
 788+ $row = $db->fetchObject( $res );
 789+ $this->attempts = intval( $row->attempts );
724790 $this->interpResult = new qp_InterpResult();
725791 $this->interpResult->short = $row->short_interpretation;
726792 $this->interpResult->long = $row->long_interpretation;
@@ -732,158 +798,131 @@
733799 // todo: change to "insert ... on duplicate key update ..." when last_insert_id() bugs will be fixed
734800 }
735801
 802+ /**
 803+ * Creates poll user from poll username.
 804+ * @param $username string qpoll username
 805+ */
 806+ function createLastUser( $username ) {
 807+ $db = wfGetDB( DB_MASTER );
 808+ # begin transaction to avoid race condition when inserting new user
 809+ $db->begin();
 810+ $this->setLastUser( $username, $db );
 811+ if ( $this->last_uid === null ) {
 812+ # user does not exist, try to create new user
 813+ $db->insert( 'qp_users',
 814+ array( 'name' => $username ),
 815+ __METHOD__ . ':UpdateUser'
 816+ );
 817+ $this->last_uid = intval( $db->insertId() );
 818+ # set username, user was created
 819+ $this->username = $username;
 820+ }
 821+ $db->commit();
 822+ }
 823+
 824+ /**
 825+ * Get username by uid
 826+ * @param $uid integer qpoll user id
 827+ */
736828 function getUserName( $uid ) {
 829+ $db = wfGetDB( DB_MASTER );
737830 if ( $uid !== null ) {
738 - $res = self::$db->select( 'qp_users', 'name', 'uid=' . self::$db->addQuotes( intval( $uid ) ), __METHOD__ );
739 - $row = self::$db->fetchObject( $res );
740 - if ( $row != false ) {
 831+ $res = $db->select(
 832+ 'qp_users',
 833+ 'name',
 834+ array( 'uid' => $uid ),
 835+ __METHOD__
 836+ );
 837+ $row = $db->fetchObject( $res );
 838+ if ( $row !== false ) {
741839 return $row->name;
742840 }
743841 }
744842 return false;
745843 }
746844
 845+ /**
 846+ * Loads poll description from DB specified by
 847+ * ($this->mArticleId, $this->mPollId).
 848+ * Optionally updates the DB, when poll tag attributes were changed.
 849+ */
747850 private function loadPid() {
748851 if ( $this->mArticleId === 0 ) {
749852 return;
750853 }
751 - $res = self::$db->select( 'qp_poll_desc',
752 - array( 'pid', 'order_id', 'dependance', 'interpretation_namespace', 'interpretation_title', 'random_question_count' ),
753 - array( 'article_id' => $this->mArticleId, 'poll_id' => $this->mPollId ),
754 - __METHOD__ );
755 - $row = self::$db->fetchObject( $res );
756 - if ( $row != false ) {
 854+ $db = wfGetDB( DB_MASTER );
 855+ if ( count( $row = qp_PollCache::load( $db ) ) > 0 ) {
757856 $this->pid = $row->pid;
758 - # some constructors don't supply the poll attributes, get the values from DB in such case
 857+ # some constructors don't supply all of the poll attributes,
 858+ # get the values from DB in such case
 859+ $updates_counter = 0;
759860 if ( $this->mOrderId === null ) {
760861 $this->mOrderId = $row->order_id;
 862+ $updates_counter++;
761863 }
762864 if ( $this->dependsOn === null ) {
763865 $this->dependsOn = $row->dependance;
 866+ $updates_counter++;
764867 }
765 - if ( is_null( $this->interpDBkey ) ) {
 868+ if ( $this->interpDBkey === null ) {
766869 $this->interpNS = $row->interpretation_namespace;
767870 $this->interpDBkey = $row->interpretation_title;
 871+ $updates_counter++;
768872 }
769 - if ( is_null( $this->randomQuestionCount ) ) {
 873+ if ( $this->randomQuestionCount === null ) {
770874 $this->randomQuestionCount = $row->random_question_count;
 875+ $updates_counter++;
771876 }
772 - $this->updatePollAttributes( $row );
 877+ if ( $updates_counter < 4 ) {
 878+ # some attributes might have been changed in poll header,
 879+ # update the cache
 880+ qp_PollCache::store( $db, 'qp_PollCache' );
 881+ }
773882 }
774883 }
775884
776 - private function setPid() {
 885+ /**
 886+ * Creates new, still non-existing poll in DB specified by
 887+ * ($this->mArticleId, $this->mPollId).
 888+ * That will also generate new poll description key in $this->pid
 889+ */
 890+ public function setPid() {
777891 if ( $this->mArticleId === 0 ) {
778892 throw new MWException( 'Cannot save new poll description during new page preprocess in ' . __METHOD__ );
779893 }
780 - $res = self::$db->select( 'qp_poll_desc',
781 - array( 'pid', 'order_id', 'dependance', 'interpretation_namespace', 'interpretation_title', 'random_question_count' ),
782 - 'article_id=' . self::$db->addQuotes( $this->mArticleId ) . ' and ' .
783 - 'poll_id=' . self::$db->addQuotes( $this->mPollId ) );
784 - $row = self::$db->fetchObject( $res );
785 - if ( $row == false ) {
786 - # paranoiac checks;
787 - # commented out because it is worth to fight bugs instead of hiding them
788 - /*
789 - if ( is_null( $this->interpDBkey ) ) {
790 - $this->interpDBkey = 0;
791 - }
792 - if ( is_null( $this->randomQuestionCount ) ) {
793 - $this->randomQuestionCount = 0;
794 - }
795 - if ( is_null( $this->dependsOn ) ) {
796 - $this->dependsOn = '';
797 - }
798 - */
799 - # end of paranoiac checks
800 - self::$db->insert( 'qp_poll_desc',
801 - array( 'article_id' => $this->mArticleId, 'poll_id' => $this->mPollId, 'order_id' => $this->mOrderId, 'dependance' => $this->dependsOn, 'interpretation_namespace' => $this->interpNS, 'interpretation_title' => $this->interpDBkey, 'random_question_count' => $this->randomQuestionCount ),
802 - __METHOD__ . ':update poll' );
803 - $this->pid = self::$db->insertId();
804 - } else {
805 - $this->pid = $row->pid;
806 - $this->updatePollAttributes( $row );
807 - }
808 -// todo: change to "insert ... on duplicate key update ..." when last_insert_id() bugs will be fixed
 894+ $db = wfGetDB( DB_MASTER );
 895+ $row = qp_PollCache::create( $db );
 896+ # set store properties unconditionally, because they are guaranteed to
 897+ # match store after qp_PollCache::create() call
 898+ $this->pid = $row->pid;
 899+ $this->mOrderId = $row->order_id;
 900+ $this->dependsOn = $row->dependance;
 901+ $this->interpNS = $row->interpretation_namespace;
 902+ $this->interpDBkey = $row->interpretation_title;
 903+ $this->randomQuestionCount = $row->random_question_count;
809904 }
810905
811 - private function updatePollAttributes( $row ) {
812 - self::$db->begin();
813 - if ( $this->mOrderId != $row->order_id ||
814 - $this->dependsOn != $row->dependance ||
815 - $this->interpNS != $row->interpretation_namespace ||
816 - $this->interpDBkey != $row->interpretation_title ||
817 - $this->randomQuestionCount != $row->random_question_count ) {
818 - $res = self::$db->replace( 'qp_poll_desc',
819 - array( 'poll', 'article_poll' ),
820 - array( 'pid' => $this->pid, 'article_id' => $this->mArticleId, 'poll_id' => $this->mPollId, 'order_id' => $this->mOrderId, 'dependance' => $this->dependsOn, 'interpretation_namespace' => $this->interpNS, 'interpretation_title' => $this->interpDBkey, 'random_question_count' => $this->randomQuestionCount ),
821 - __METHOD__ . ':poll attributes update'
822 - );
 906+ /**
 907+ * Creates / stores question answer from question instance into
 908+ * question data instance.
 909+ * @param $question qp_StubQuestion
 910+ * instance of question which has current user vote
 911+ */
 912+ public function setQuestionAnswer( qp_StubQuestion $question ) {
 913+ if ( $this->questionExists( $question->mQuestionId ) ) {
 914+ # question data already exists, poll structure was stored during previous
 915+ # submission.
 916+ # question, category and proposal descriptions are already loaded into
 917+ # $this->Questions[$question->mQuestionId] by $this->loadQuestions()
 918+ $this->Questions[$question->mQuestionId]->setQuestionAnswer( $question );
 919+ } else {
 920+ # create new question data from scratch (first submission)
 921+ $this->Questions[$question->mQuestionId] = qp_QuestionData::factory( $question );
823922 }
824 - if ( $this->randomQuestionCount != $row->random_question_count &&
825 - $this->randomQuestionCount == 0 &&
826 - self::$purgeRandomQuestions ) {
827 - # the poll questions are not randomized anymore
828 - self::$db->delete( 'qp_random_questions',
829 - array( 'pid' => $this->pid ),
830 - __METHOD__ . ':delete unused random seeds' );
831 - }
832 - self::$db->commit();
833923 }
834924
835 - private function setQuestionDesc() {
836 - global $wgContLang;
837 - $insert = array();
838 - foreach ( $this->Questions as $qkey => &$ques ) {
839 - $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'type' => $ques->type, 'common_question' => $wgContLang->truncate( $ques->CommonQuestion, qp_Setup::$field_max_len['common_question'] , '' ) );
840 - $ques->question_id = $qkey;
841 - }
842 - if ( count( $insert ) > 0 ) {
843 - self::$db->replace( 'qp_question_desc',
844 - array( 'question' ),
845 - $insert,
846 - __METHOD__ );
847 - }
848 - }
849 -
850 - private function setCategories() {
851 - $insert = Array();
852 - foreach ( $this->Questions as $qkey => &$ques ) {
853 - $ques->packSpans();
854 - foreach ( $ques->Categories as $catkey => &$Cat ) {
855 - $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'cat_id' => $catkey, 'cat_name' => $Cat["name"] );
856 - }
857 - $ques->restoreSpans();
858 - }
859 - if ( count( $insert ) > 0 ) {
860 - self::$db->replace( 'qp_question_categories',
861 - array( 'category' ),
862 - $insert,
863 - __METHOD__ );
864 - }
865 - }
866 -
867 - private function setProposals() {
868 - global $wgContLang;
869 - $insert = Array();
870 - foreach ( $this->Questions as $qkey => &$ques ) {
871 - foreach ( $ques->ProposalText as $propkey => $ptext ) {
872 - if ( isset( $ques->ProposalNames[$propkey] ) ) {
873 - $ptext = qp_QuestionData::getProposalNamePrefix( $ques->ProposalNames[$propkey] ) . $ptext;
874 - }
875 - $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'proposal_id' => $propkey, 'proposal_text' => $wgContLang->truncate( $ptext, qp_Setup::$field_max_len['proposal_text'] , '' ) );
876 - }
877 - }
878 - if ( count( $insert ) > 0 ) {
879 - self::$db->replace( 'qp_question_proposals',
880 - array( 'proposal' ),
881 - $insert,
882 - __METHOD__ );
883 - }
884 - }
885 -
886925 /**
887 - * Prepares an array of user answer to the current poll and interprets these
 926+ * Prepares an array of user answer to the current poll and interprets these.
888927 * Stores the result in $this->interpResult
889928 */
890929 private function interpretVote() {
@@ -903,7 +942,7 @@
904943 # prepare array of user answers that will be passed to the interpreter
905944 $poll_answer = array();
906945
907 - foreach ( $this->Questions as &$qdata ) {
 946+ foreach ( $this->Questions as $qdata ) {
908947 if ( !$this->isUsedQuestion( $qdata->question_id ) ) {
909948 continue;
910949 }
@@ -930,56 +969,79 @@
931970 $this->interpResult = qp_Interpret::getResult( $interpArticle, array( 'answer' => $poll_answer, 'randomQuestions' => $this->randomQuestions ) );
932971 }
933972
934 - // warning: requires qp_PollStorage::last_uid to be set
935 - private function setAnswers() {
936 - $insert = Array();
937 - foreach ( $this->Questions as $qkey => &$ques ) {
 973+ /**
 974+ * Actually stores user answers to all the questions of current poll.
 975+ * @param $db instance of DB_MASTER (transaction in progress)
 976+ * Warning: requires qp_PollStorage::last_uid to be set.
 977+ */
 978+ private function setAnswers( $db ) {
 979+ $insert = array();
 980+ foreach ( $this->Questions as $qkey => $ques ) {
938981 foreach ( $ques->ProposalCategoryId as $propkey => &$prop_answers ) {
939982 foreach ( $prop_answers as $idkey => $catkey ) {
940 - $insert[] = array( 'uid' => $this->last_uid, 'pid' => $this->pid, 'question_id' => $qkey, 'proposal_id' => $propkey, 'cat_id' => $catkey, 'text_answer' => $ques->ProposalCategoryText[ $propkey ][ $idkey ] );
 983+ $insert[] = array(
 984+ 'uid' => $this->last_uid,
 985+ 'pid' => $this->pid,
 986+ 'question_id' => $qkey,
 987+ 'proposal_id' => $propkey,
 988+ 'cat_id' => $catkey,
 989+ 'text_answer' => $ques->ProposalCategoryText[$propkey][$idkey]
 990+ );
941991 }
942992 }
943993 }
944 - # TODO: delete votes of all users, when the POST question header is incompatible with question header in DB ?
945 - # delete previous vote to make sure previous header of this poll was not incompatible with current vote
946 - self::$db->delete( 'qp_question_answers',
 994+ # Delete previous user vote to make sure previous definitions of this poll
 995+ # questions are not incompatible to their current definitions.
 996+ $db->delete( 'qp_question_answers',
947997 array( 'uid' => $this->last_uid, 'pid' => $this->pid ),
948998 __METHOD__ . ':delete previous answers of current user to the same poll'
949999 );
9501000 # vote
9511001 if ( count( $insert ) > 0 ) {
952 - self::$db->replace( 'qp_question_answers',
 1002+ $db->replace( 'qp_question_answers',
9531003 array( 'answer' ),
9541004 $insert,
955 - __METHOD__ );
 1005+ __METHOD__
 1006+ );
 1007+ $this->attempts++;
9561008 # update interpretation result and number of syntax-valid resubmit attempts
957 - $qp_users_polls = self::$db->tableName( 'qp_users_polls' );
958 - $short = self::$db->addQuotes( $this->interpResult->short );
959 - $long = self::$db->addQuotes( $this->interpResult->long );
960 - $structured = self::$db->addQuotes( $this->interpResult->structured );
961 - $this->attempts++;
962 - $stmt = "INSERT INTO {$qp_users_polls} (uid,pid,short_interpretation,long_interpretation,structured_interpretation)\n VALUES ( " . intval( $this->last_uid ) . ", " . intval( $this->pid ) . ", {$short}, {$long}, {$structured} )\n ON DUPLICATE KEY UPDATE attempts = " . intval( $this->attempts ) . ", short_interpretation = {$short} , long_interpretation = {$long}, structured_interpretation = {$structured}";
963 - self::$db->query( $stmt, __METHOD__ );
 1009+ $qp_users_polls = $db->tableName( 'qp_users_polls' );
 1010+ $uid = $db->addQuotes( $this->last_uid );
 1011+ $pid = $db->addQuotes( $this->pid );
 1012+ $short = $db->addQuotes( $this->interpResult->short );
 1013+ $long = $db->addQuotes( $this->interpResult->long );
 1014+ $structured = $db->addQuotes( $this->interpResult->structured );
 1015+ $attempts = $db->addQuotes( $this->attempts );
 1016+ $stmt = "INSERT INTO {$qp_users_polls}
 1017+(uid, pid, short_interpretation, long_interpretation, structured_interpretation)
 1018+VALUES ( {$uid}, {$pid}, {$short}, {$long}, {$structured} )
 1019+ON DUPLICATE KEY UPDATE
 1020+attempts = {$attempts}, short_interpretation = {$short} , long_interpretation = {$long}, structured_interpretation = {$structured}";
 1021+ $db->query( $stmt, __METHOD__ );
9641022 }
9651023 }
9661024
967 - # when the user votes and poll wasn't previousely voted yet, it also creates the poll structures in DB
 1025+ /**
 1026+ * Complete storage of user vote into DB. Final stage of successful poll POST.
 1027+ * When current poll wasn't previousely voted yet, it also creates poll structure
 1028+ * in DB
 1029+ */
9681030 function setUserVote() {
969 - if ( $this->pid !== null &&
 1031+ if ( $this->hasQuestions() &&
9701032 $this->last_uid !== null &&
971 - $this->mCompletedPostData == "complete" &&
972 - is_array( $this->Questions ) && count( $this->Questions ) > 0 ) {
973 - self::$db->begin();
974 - $this->setQuestionDesc();
975 - $this->setCategories();
976 - $this->setProposals();
 1033+ $this->mCompletedPostData === 'complete'
 1034+ ) {
9771035 $this->interpretVote();
 1036+ # warning: transaction should include minimal set of carefully monitored methods
 1037+ $db = wfGetDB( DB_MASTER );
 1038+ $db->begin();
 1039+ qp_PollCache::store( $db, 'qp_QuestionCache', 'qp_CategoryCache', 'qp_ProposalCache' );
9781040 if ( $this->interpResult->hasToBeStored() ) {
979 - $this->setAnswers();
 1041+ $this->setAnswers( $db );
9801042 }
981 - self::$db->commit();
 1043+ $db->commit();
9821044 $this->voteDone = true;
9831045 }
9841046 }
9851047
986 -}
 1048+} /* enf of qp_PollStore class */
Index: trunk/extensions/QPoll/specials/qp_results.php
@@ -209,7 +209,7 @@
210210 }
211211 $userTitle = Title::makeTitleSafe( NS_USER, $userName );
212212 $user_link = $this->qpLink( $userTitle, $userName );
213 - $pollStore->setLastUser( $userName, false );
 213+ $pollStore->setLastUser( $userName );
214214 if ( !$pollStore->loadUserVote() ) {
215215 return '';
216216 }
@@ -244,7 +244,7 @@
245245 $poll_link = $this->qpLink( $poll_title, $poll_title->getPrefixedText() . wfMsg( 'word-separator' ) . wfMsg( 'qp_parentheses', $pollStore->mPollId ) );
246246 $output .= wfMsg( 'qp_browse_to_poll', $poll_link ) . "<br />\n";
247247 $interpTitle = $pollStore->getInterpTitle();
248 - if ( $interpTitle !== null ) {
 248+ if ( $interpTitle instanceof Title ) {
249249 $interp_link = $this->qpLink( $interpTitle, $interpTitle->getPrefixedText() );
250250 $output .= wfMsg( 'qp_browse_to_interpretation', $interp_link ) . "<br />\n";
251251 }
@@ -293,7 +293,7 @@
294294 # statistics export uses additional formats
295295 $percent_num_format = '[Blue]0.0%;[Red]-0.0%;[Black]0%';
296296 $qp_xls->addFormats( array(
297 - 'percent' => array( 'fgcolor' => 0x1A, 'border' => 1 )
 297+ 'percent' => $qp_xls->getFormatDefinition( 'answer' )
298298 ) );
299299 $qp_xls->getFormat( 'percent' )->setAlign( 'left' );
300300 $qp_xls->getFormat( 'percent' )->setNumFormat( $percent_num_format );
@@ -354,17 +354,17 @@
355355
356356 function getIntervalResults( $offset, $limit ) {
357357 $result = array();
358 - $db = & wfGetDB( DB_SLAVE );
359 - $qp_users = $db->tableName( 'qp_users' );
360 - $qp_users_polls = $db->tableName( 'qp_users_polls' );
361 - $res = $db->select( "$qp_users_polls qup, $qp_users qu",
 358+ $db = wfGetDB( DB_SLAVE );
 359+ $res = $db->select( array( 'qup' => 'qp_users_polls', 'qu' => 'qp_users' ),
362360 array( 'qu.uid as uid', 'name as username', 'count(pid) as pidcount' ),
363 - 'qu.uid=qup.uid',
 361+ /* WHERE */ 'qu.uid=qup.uid',
364362 __METHOD__,
365 - array( 'GROUP BY' => 'qup.uid',
366 - 'ORDER BY' => $this->order_by,
367 - 'OFFSET' => intval( $offset ),
368 - 'LIMIT' => intval( $limit ) )
 363+ array(
 364+ 'GROUP BY' => 'qup.uid',
 365+ 'ORDER BY' => $this->order_by,
 366+ 'OFFSET' => $offset,
 367+ 'LIMIT' => $limit
 368+ )
369369 );
370370 while ( $row = $db->fetchObject( $res ) ) {
371371 $result[] = $row;
@@ -418,12 +418,13 @@
419419 # fake pollStore to get username by uid: avoid to use this trick as much as possible
420420 $pollStore = new qp_PollStore();
421421 $userName = $pollStore->getUserName( $this->uid );
422 - $db = & wfGetDB( DB_SLAVE );
 422+ $db = wfGetDB( DB_SLAVE );
423423 $res = $db->select(
424 - array( 'qp_users_polls' ),
 424+ 'qp_users_polls',
425425 array( 'count(pid) as pidcount' ),
426 - 'uid=' . $db->addQuotes( $this->uid ),
427 - __METHOD__ );
 426+ /* WHERE */ array( 'uid' => $this->uid ),
 427+ __METHOD__
 428+ );
428429 if ( $row = $db->fetchObject( $res ) ) {
429430 $pidcount = $row->pidcount;
430431 } else {
@@ -438,18 +439,18 @@
439440
440441 function getIntervalResults( $offset, $limit ) {
441442 $result = Array();
442 - $db = & wfGetDB( DB_SLAVE );
 443+ $db = wfGetDB( DB_SLAVE );
443444 $page = $db->tableName( 'page' );
444445 $qp_poll_desc = $db->tableName( 'qp_poll_desc' );
445446 $qp_users_polls = $db->tableName( 'qp_users_polls' );
446 - $query = "SELECT pid, page_namespace AS ns, page_title AS title, poll_id ";
447 - $query .= "FROM ($qp_poll_desc, $page) ";
448 - $query .= " WHERE page_id=article_id AND pid " . ( $this->inverse ? "NOT " : "" ) . "IN ";
449 - $query .= "(SELECT pid ";
450 - $query .= "FROM $qp_users_polls ";
451 - $query .= "WHERE uid=" . $db->addQuotes( $this->uid ) . ") ";
452 - $query .= "ORDER BY page_namespace, page_title, poll_id ";
453 - $query .= "LIMIT " . intval( $offset ) . ", " . intval( $limit );
 447+ $query = "SELECT pid, page_namespace AS ns, page_title AS title, poll_id " .
 448+ "FROM ({$qp_poll_desc}, {$page}) " .
 449+ " WHERE page_id=article_id AND pid " . ( $this->inverse ? "NOT " : "" ) . "IN " .
 450+ "(SELECT pid " .
 451+ "FROM {$qp_users_polls} " .
 452+ "WHERE uid=" . $db->addQuotes( $this->uid ) . ") " .
 453+ "ORDER BY page_namespace, page_title, poll_id " .
 454+ "LIMIT " . intval( $offset ) . ", " . intval( $limit );
454455 $res = $db->query( $query, __METHOD__ );
455456 while ( $row = $db->fetchObject( $res ) ) {
456457 $result[] = $row;
@@ -485,15 +486,17 @@
486487
487488 function getIntervalResults( $offset, $limit ) {
488489 $result = array();
489 - $db = & wfGetDB( DB_SLAVE );
 490+ $db = wfGetDB( DB_SLAVE );
490491 $res = $db->select(
491492 array( 'page', 'qp_poll_desc' ),
492493 array( 'page_namespace as ns', 'page_title as title', 'pid', 'poll_id', 'order_id' ),
493 - 'page_id=article_id',
 494+ /* WHERE */ 'page_id=article_id',
494495 __METHOD__,
495 - array( 'ORDER BY' => 'page_namespace, page_title, order_id',
496 - 'OFFSET' => intval( $offset ),
497 - 'LIMIT' => intval( $limit ) )
 496+ array(
 497+ 'ORDER BY' => 'page_namespace, page_title, order_id',
 498+ 'OFFSET' => $offset,
 499+ 'LIMIT' => $limit
 500+ )
498501 );
499502 while ( $row = $db->fetchObject( $res ) ) {
500503 $result[] = $row;
@@ -539,12 +542,13 @@
540543 function getPageHeader() {
541544 global $wgLang, $wgContLang;
542545 $link = "";
543 - $db = & wfGetDB( DB_SLAVE );
 546+ $db = wfGetDB( DB_SLAVE );
544547 $res = $db->select(
545548 array( 'page', 'qp_poll_desc' ),
546549 array( 'page_namespace as ns', 'page_title as title', 'poll_id' ),
547 - 'page_id=article_id and pid=' . $db->addQuotes( $this->pid ),
548 - __METHOD__ );
 550+ /* WHERE */ 'page_id=article_id and pid=' . $db->addQuotes( $this->pid ),
 551+ __METHOD__
 552+ );
549553 if ( $row = $db->fetchObject( $res ) ) {
550554 $poll_title = Title::makeTitle( intval( $row->ns ), $row->title, qp_AbstractPoll::s_getPollTitleFragment( $row->poll_id, '' ) );
551555 $pagename = qp_Setup::specialchars( $wgContLang->convert( $poll_title->getPrefixedText() ) );
@@ -562,15 +566,15 @@
563567
564568 function getIntervalResults( $offset, $limit ) {
565569 $result = Array();
566 - $db = & wfGetDB( DB_SLAVE );
 570+ $db = wfGetDB( DB_SLAVE );
567571 $qp_users = $db->tableName( 'qp_users' );
568572 $qp_users_polls = $db->tableName( 'qp_users_polls' );
569 - $query = "SELECT uid, name as username ";
570 - $query .= "FROM $qp_users ";
571 - $query .= "WHERE uid " . ( $this->inverse ? "NOT " : "" ) . "IN ";
572 - $query .= "(SELECT uid FROM $qp_users_polls WHERE pid=" . $db->addQuotes( $this->pid ) . ") ";
573 - $query .= "ORDER BY uid ";
574 - $query .= "LIMIT " . intval( $offset ) . ", " . intval( $limit );
 573+ $query = "SELECT uid, name as username " .
 574+ "FROM {$qp_users} " .
 575+ "WHERE uid " . ( $this->inverse ? "NOT " : "" ) . "IN " .
 576+ "(SELECT uid FROM {$qp_users_polls} WHERE pid=" . $db->addQuotes( $this->pid ) . ") " .
 577+ "ORDER BY uid " .
 578+ "LIMIT " . intval( $offset ) . ", " . intval( $limit );
575579 $res = $db->query( $query, __METHOD__ );
576580 while ( $row = $db->fetchObject( $res ) ) {
577581 $result[] = $row;
@@ -618,13 +622,12 @@
619623 $this->question_id = $question_id;
620624 $this->proposal_id = $proposal_id;
621625 $this->cat_id = $cid;
622 - $db = & wfGetDB( DB_SLAVE );
623 - $qp_poll_desc = $db->tableName( 'qp_poll_desc' );
624 - $page = $db->tableName( 'page' );
625 - $query = "SELECT pid, page_namespace as ns, page_title as title, poll_id ";
626 - $query .= "FROM ($qp_poll_desc, $page) ";
627 - $query .= "WHERE page_id=article_id AND pid=" . $db->addQuotes( $pid ) . "";
628 - $res = $db->query( $query, __METHOD__ );
 626+ $db = wfGetDB( DB_SLAVE );
 627+ $res = $db->select( array( 'qp_poll_desc', 'page' ),
 628+ array( 'pid', 'page_namespace as ns', 'page_title as title', 'poll_id' ),
 629+ /* WHERE */ 'page_id=article_id AND pid=' . $db->addQuotes( $pid ),
 630+ __METHOD__
 631+ );
629632 if ( $row = $db->fetchObject( $res ) ) {
630633 $this->pid = intval( $row->pid );
631634 $this->ns = intval( $row->ns );
@@ -636,7 +639,6 @@
637640 function getPageHeader() {
638641 global $wgLang, $wgContLang;
639642 $link = "";
640 - $db = & wfGetDB( DB_SLAVE );
641643 if ( $this->pid !== null ) {
642644 $pollStore = new qp_PollStore( array( 'from' => 'pid', 'pid' => $this->pid ) );
643645 if ( $pollStore->pid !== null ) {
@@ -683,15 +685,17 @@
684686
685687 function getIntervalResults( $offset, $limit ) {
686688 $result = Array();
687 - $db = & wfGetDB( DB_SLAVE );
688 - $qp_users = $db->tableName( 'qp_users' );
689 - $qp_question_answers = $db->tableName( 'qp_question_answers' );
690 - $query = "SELECT qqa.uid as uid, name as username, text_answer ";
691 - $query .= "FROM $qp_question_answers qqa ";
692 - $query .= "INNER JOIN $qp_users qu ON qqa.uid = qu.uid ";
693 - $query .= "WHERE pid=" . $db->addQuotes( $this->pid ) . " AND question_id=" . $db->addQuotes( $this->question_id ) . " AND proposal_id=" . $db->addQuotes( $this->proposal_id ) . " AND cat_id=" . $db->addQuotes( $this->cat_id ) . " ";
694 - $query .= "LIMIT " . intval( $offset ) . ", " . intval( $limit );
695 - $res = $db->query( $query, __METHOD__ );
 689+ $db = wfGetDB( DB_SLAVE );
 690+ $res = $db->select(
 691+ array( 'qqa' => 'qp_question_answers', 'qu' => 'qp_users' ),
 692+ array( 'qqa.uid as uid', 'name as username', 'text_answer' ),
 693+ /* WHERE */ array( 'pid' => $this->pid, 'question_id' => $this->question_id, 'proposal_id' => $this->proposal_id, 'cat_id' => $this->cat_id ),
 694+ __METHOD__,
 695+ array( 'OFFSET' => $offset, 'LIMIT' => $limit ),
 696+ /* JOIN */ array(
 697+ 'qu' => array( 'INNER JOIN', 'qqa.uid = qu.uid' )
 698+ )
 699+ );
696700 while ( $row = $db->fetchObject( $res ) ) {
697701 $result[] = $row;
698702 }
Index: trunk/extensions/QPoll/ctrl/poll/qp_abstractpoll.php
@@ -100,9 +100,8 @@
101101 * @public
102102 */
103103 function __construct( $argv, qp_AbstractPollView $view ) {
104 - global $wgRequest, $wgLanguageCode;
105 - $this->mRequest = &$wgRequest;
106 - $this->mResponse = $wgRequest->response();
 104+ global $wgLanguageCode;
 105+ $this->mResponse = qp_Setup::$request->response();
107106 # Determine which messages will be used, according to the language.
108107 qp_Setup::onLoadAllMessages();
109108 $view->setController( $this );
@@ -220,7 +219,6 @@
221220 // array[2] - prefixed (complete) poll address
222221 // false - invalid source poll address was given
223222 static function getPrefixedPollAddress( $addr ) {
224 - global $wgTitle;
225223 if ( is_array( $addr ) ) {
226224 if ( count( $addr ) > 1 ) {
227225 list( $titlePart, $pollIdPart ) = $addr;
@@ -241,7 +239,7 @@
242240 }
243241 if ( $titlePart == '' ) {
244242 # poll is located at the current page
245 - $titlePart = $wgTitle->getPrefixedText();
 243+ $titlePart = qp_Setup::$title->getPrefixedText();
246244 }
247245 return array( $titlePart, $pollIdPart, $titlePart . '#' . $pollIdPart );
248246 }
Index: trunk/extensions/QPoll/ctrl/poll/qp_pollstats.php
@@ -78,7 +78,7 @@
7979 $this->mState = "error";
8080 return self::fatalErrorQuote( 'qp_error_no_stats', $this->pollAddr );
8181 }
82 - $this->pollStore->setLastUser( $this->username, false );
 82+ $this->pollStore->setLastUser( $this->username );
8383 # do not check the result, because we may show results even if the user hasn't voted
8484 $this->pollStore->loadUserAlreadyVoted();
8585 return true;
Index: trunk/extensions/QPoll/ctrl/poll/qp_poll.php
@@ -97,7 +97,7 @@
9898 # order_id is used to sort out polls on the Special:PollResults statistics page
9999 $this->mOrderId = self::$sOrderId;
100100 # Determine if this poll is being corrected or not, according to the pollId
101 - $this->mBeingCorrected = ( $this->mRequest->getVal( 'pollId' ) == $this->mPollId );
 101+ $this->mBeingCorrected = ( qp_Setup::$request->wasPosted() && qp_Setup::$request->getVal( 'pollId' ) == $this->mPollId );
102102 }
103103
104104 /**
@@ -157,13 +157,13 @@
158158 $newPollStore['from'] = 'poll_post';
159159 $this->pollStore = new qp_PollStore( $newPollStore );
160160 $this->pollStore->loadQuestions();
161 - $this->pollStore->setLastUser( $this->username, false );
 161+ $this->pollStore->setLastUser( $this->username );
162162 $this->pollStore->loadUserAlreadyVoted();
163163 } else {
164164 $newPollStore['from'] = 'poll_get';
165165 $this->pollStore = new qp_PollStore( $newPollStore );
166166 $this->pollStore->loadQuestions();
167 - $this->pollStore->setLastUser( $this->username, false );
 167+ $this->pollStore->setLastUser( $this->username );
168168 # to show previous choice of current user, if that's available
169169 # do not check the result, because user can vote even if haven't voted before
170170 $this->pollStore->loadUserVote();
@@ -204,8 +204,11 @@
205205 $this->questions->randomize( $this->randomQuestionCount );
206206 $this->pollStore->randomQuestions = $this->questions->getUsedQuestions();
207207 # store random questions for current user into DB
208 - $this->pollStore->setLastUser( $this->username );
209 - $this->pollStore->setRandomQuestions();
 208+ if ( qp_Setup::$title->getArticleID() !== 0 ) {
 209+ $this->pollStore->setPid();
 210+ $this->pollStore->createLastUser( $this->username );
 211+ $this->pollStore->setRandomQuestions();
 212+ }
210213 }
211214
212215 /**
@@ -215,7 +218,6 @@
216219 * @return boolean true - stop further processing, false - continue processing
217220 */
218221 function parseInput( $input ) {
219 - global $wgTitle;
220222 # parse the input; generates $this->questions collection
221223 $this->parseQuestionsHeaders( $input );
222224 $this->setUsedQuestions();
@@ -228,7 +230,7 @@
229231 }
230232 if ( $this->pollStore->stateComplete() ) {
231233 # store user vote to the DB (when the poll is fine)
232 - $this->pollStore->setLastUser( $this->username );
 234+ $this->pollStore->createLastUser( $this->username );
233235 $this->pollStore->setUserVote();
234236 }
235237 if ( $this->pollStore->interpResult->isError() ) {
@@ -240,7 +242,7 @@
241243 $this->mResponse->setcookie( 'QPoll', 'clearCache', time() + 20 );
242244 }
243245 $this->mResponse->header( 'HTTP/1.0 302 Found' );
244 - $this->mResponse->header( 'Location: ' . $wgTitle->getFullURL() . $this->getPollTitleFragment() );
 246+ $this->mResponse->header( 'Location: ' . qp_Setup::$title->getFullURL() . $this->getPollTitleFragment() );
245247 return true;
246248 }
247249 return false;
@@ -269,7 +271,7 @@
270272 if ( !$depPollStore->loadQuestions() ) {
271273 return self::fatalErrorNoQuote( 'qp_error_vote_dependance_poll', $depLink );
272274 }
273 - $depPollStore->setLastUser( $this->username, false );
 275+ $depPollStore->setLastUser( $this->username );
274276 if ( $depPollStore->loadUserAlreadyVoted() ) {
275277 # user already voted in current the poll in chain
276278 if ( $depPollStore->dependsOn === '' ) {
@@ -471,7 +473,7 @@
472474 if ( $this->mBeingCorrected ) {
473475 if ( $question->getState() == '' ) {
474476 # question is OK, store it into pollStore
475 - $question->store( $this->pollStore );
 477+ $this->pollStore->setQuestionAnswer( $question );
476478 } else {
477479 # http post: not every proposals were answered: do not update DB
478480 $this->pollStore->stateIncomplete();
Index: trunk/extensions/QPoll/ctrl/question/qp_abstractquestion.php
@@ -45,8 +45,6 @@
4646 * @param $questionId the identifier of the question used to generate input names
4747 */
4848 function __construct( qp_AbstractPoll $poll, qp_AbstractView $view, $questionId ) {
49 - global $wgRequest;
50 - $this->mRequest = &$wgRequest;
5149 # the question collection is not sparce by default
5250 $this->mQuestionId = $this->usedId = $questionId;
5351 $view->setController( $this );
Index: trunk/extensions/QPoll/ctrl/question/qp_textquestion.php
@@ -296,11 +296,11 @@
297297 $text_answer = '';
298298 # try to load from POST data
299299 if ( $this->poll->mBeingCorrected &&
300 - ( $ta = $this->mRequest->getArray( $name ) ) !== null ) {
 300+ ( $ta = qp_Setup::$request->getArray( $name ) ) !== null ) {
301301 if ( $opt->type === 'text' ) {
302302 if ( count( $ta ) === 1 ) {
303303 # fallback to WebRequest::getText(), because it offers useful preprocessing
304 - $ta = trim( $this->mRequest->getText( $name ) );
 304+ $ta = trim( qp_Setup::$request->getText( $name ) );
305305 } else {
306306 # pack select multiple values
307307 $ta = implode( qp_Setup::SELECT_MULTIPLE_VALUES_SEPARATOR, array_map( 'trim', $ta ) );
@@ -466,7 +466,12 @@
467467 $opt->reset();
468468 $this->propview = new qp_TextQuestionProposalView( $proposalId, $this );
469469 # set proposal name (if any)
470 - $prop_name = qp_QuestionData::splitRawProposal( $raw );
 470+ if ( ( $prop_name = qp_QuestionData::splitRawProposal( $raw ) ) === false ) {
 471+ # we do not need to generate error for too long proposal name,
 472+ # because the length limit will be enforced on the whole serialized
 473+ # proposal string (with proposal_name + cat_parts + prop_parts)
 474+ $prop_name = '';
 475+ }
471476 $this->dbtokens = $brace_stack = array();
472477 $dbtokens_idx = -1;
473478 $catId = 0;
Index: trunk/extensions/QPoll/ctrl/question/qp_mixedquestion.php
@@ -43,7 +43,10 @@
4444 }
4545 $proposalId++;
4646 # set proposal name (if any)
47 - if ( ( $prop_name = qp_QuestionData::splitRawProposal( $pview->text ) ) !== '' ) {
 47+ $prop_name = qp_QuestionData::splitRawProposal( $pview->text );
 48+ if ( $prop_name === false ) {
 49+ $pview->prependErrorMessage( wfMsg( 'qp_error_too_long_proposal_name' ), 'error' );
 50+ } elseif ( $prop_name !== '' ) {
4851 $this->mProposalNames[$proposalId] = $prop_name;
4952 }
5053 $this->mProposalText[$proposalId] = trim( $pview->text );
@@ -74,9 +77,9 @@
7578 # Determine if the input has to be checked.
7679 $input_checked = false;
7780 $text_answer = '';
78 - if ( $this->poll->mBeingCorrected && $this->mRequest->getVal( $name ) !== null ) {
 81+ if ( $this->poll->mBeingCorrected && qp_Setup::$request->getVal( $name ) !== null ) {
7982 if ( $inputType == 'text' ) {
80 - $text_answer = trim( $this->mRequest->getText( $name ) );
 83+ $text_answer = trim( qp_Setup::$request->getText( $name ) );
8184 if ( strlen( $text_answer ) > qp_Setup::$field_max_len['text_answer'] ) {
8285 $text_answer = $wgContLang->truncate( $text_answer, qp_Setup::$field_max_len['text_answer'] , '' );
8386 }
Index: trunk/extensions/QPoll/ctrl/question/qp_tabularquestion.php
@@ -282,7 +282,10 @@
283283 $proposalId++;
284284 $pview->text = array_pop( $matches );
285285 # set proposal name (if any)
286 - if ( ( $prop_name = qp_QuestionData::splitRawProposal( $pview->text ) ) !== '' ) {
 286+ $prop_name = qp_QuestionData::splitRawProposal( $pview->text );
 287+ if ( $prop_name === false ) {
 288+ $pview->prependErrorMessage( wfMsg( 'qp_error_too_long_proposal_name' ), 'error' );
 289+ } elseif ( $prop_name !== '' ) {
287290 $this->mProposalNames[$proposalId] = $prop_name;
288291 }
289292 $this->mProposalText[$proposalId] = trim( $pview->text );
@@ -305,7 +308,7 @@
306309 break;
307310 }
308311 # Determine if the input had to be checked.
309 - if ( $this->poll->mBeingCorrected && $this->mRequest->getVal( $name ) == $value ) {
 312+ if ( $this->poll->mBeingCorrected && qp_Setup::$request->getVal( $name ) == $value ) {
310313 $inp[ 'checked' ] = 'checked';
311314 }
312315 if ( $this->answerExists( $inputType, $proposalId, $catId ) !== false ) {
Index: trunk/extensions/QPoll/ctrl/question/qp_stubquestion.php
@@ -85,25 +85,6 @@
8686 return false;
8787 }
8888
89 - # store the proper (checked) Question
90 - # creates new qp_QuestionData in the given poll store
91 - # and places it into the poll store Questions[] collection
92 - # @param the object of type qp_PollStore
93 - function store( qp_PollStore &$pollStore ) {
94 - if ( $pollStore->pid !== null ) {
95 - $pollStore->Questions[ $this->mQuestionId ] = qp_PollStore::newQuestionData( array(
96 - 'from' => 'postdata',
97 - 'type' => $this->mType,
98 - 'common_question' => $this->mCommonQuestion,
99 - 'categories' => $this->mCategories,
100 - 'category_spans' => $this->mCategorySpans,
101 - 'proposal_text' => $this->mProposalText,
102 - 'proposal_names' => $this->mProposalNames,
103 - 'proposal_category_id' => $this->mProposalCategoryId,
104 - 'proposal_category_text' => $this->mProposalCategoryText ) );
105 - }
106 - }
107 -
10889 function isUniqueProposalCategoryId( $proposalId, $catId ) {
10990 foreach ( $this->mProposalCategoryId as $proposalCategoryId ) {
11091 if ( in_array( $catId, $proposalCategoryId ) ) {
Index: trunk/extensions/QPoll/qp_user.php
@@ -128,7 +128,7 @@
129129 }
130130 $username = qp_Setup::getCurrUserName();
131131 $pollStore->loadQuestions();
132 - $pollStore->setLastUser( $username, false );
 132+ $pollStore->setLastUser( $username );
133133 if ( $pollStore->interpResult->structured === '' ) {
134134 return null;
135135 }
@@ -141,10 +141,11 @@
142142 */
143143 class qp_Setup {
144144
 145+ # internal unique error codes
145146 const ERROR_MISSED_TITLE = 1;
146147 const ERROR_INVALID_ADDRESS = 2;
147148
148 - # unicode character used to display selected checkboxes and radiobuttons in
 149+ # unicode entity used to display selected checkboxes and radiobuttons in
149150 # result views at Special:Pollresults page
150151 const RESULTS_CHECK_SIGN = '&#9734;';
151152
@@ -168,10 +169,14 @@
169170 static $ExtDir; // filesys path with windows path fix
170171 static $ScriptPath; // apache virtual path
171172 static $messagesLoaded = false; // check whether the extension's localized messages are loaded
172 - static $article; // Article instance we got from hook parameter
173 - static $title; // Title instance we got from hook parameter
174 - static $user; // User instance we got from hook parameter
175173
 174+ # current context
 175+ static $output; // OutputPage instance recieved from hook
 176+ static $article; // Article instance recieved from hook
 177+ static $title; // Title instance recieved from hook
 178+ static $user; // User instance recieved from hook
 179+ static $request; // WebRequest instance recieved from hook
 180+
176181 /**
177182 * The map of question 'type' attribute value to the question's ctrl / view / subtype.
178183 */
@@ -274,6 +279,11 @@
275280 );
276281 /* end of default configuration settings */
277282
 283+ # unicode character used to display selected checkboxes and radiobuttons in
 284+ # result views at Special:Pollresults page
 285+ static $resultsCheckCode = '+';
 286+
 287+
278288 static function entities( $s ) {
279289 return htmlentities( $s, ENT_QUOTES, 'UTF-8' );
280290 }
@@ -282,6 +292,10 @@
283293 return htmlentities( $s, ENT_QUOTES, 'UTF-8' );
284294 }
285295
 296+ static function entity_decode( $s ) {
 297+ return html_entity_decode( $s, ENT_QUOTES, 'UTF-8' );
 298+ }
 299+
286300 /**
287301 * Autoload classes from the map provided
288302 */
@@ -316,6 +330,8 @@
317331 $top_dir = array_pop( $dirs );
318332 self::$ScriptPath = $wgScriptPath . '/extensions' . ( ( $top_dir == 'extensions' ) ? '' : '/' . $top_dir );
319333
 334+ self::$resultsCheckCode = self::entity_decode( self::RESULTS_CHECK_SIGN );
 335+
320336 # language files
321337 # extension messages
322338 $wgExtensionMessagesFiles['QPoll'] = self::$ExtDir . '/i18n/qp.i18n.php';
@@ -326,7 +342,7 @@
327343
328344 # extension setup, hooks handling and content transformation
329345 self::autoLoad( array(
330 - 'qp_user.php' => 'qp_Setup',
 346+ 'qp_user.php' => __CLASS__,
331347 'includes/qp_functionshook.php' => 'qp_FunctionsHook',
332348 'includes/qp_renderer.php' => 'qp_Renderer',
333349 'includes/qp_xlswriter.php' => 'qp_XlsWriter',
@@ -381,6 +397,12 @@
382398 ## models/storage
383399 # poll
384400 'model/qp_pollstore.php' => 'qp_PollStore',
 401+ ## memory cache / database storage for poll / question / category / proposal
 402+ ## descriptions
 403+ 'model/cache/qp_pollcache.php' => 'qp_PollCache',
 404+ 'model/cache/qp_questioncache.php' => 'qp_QuestionCache',
 405+ 'model/cache/qp_categorycache.php' => 'qp_CategoryCache',
 406+ 'model/cache/qp_proposalcache.php' => 'qp_ProposalCache',
385407 # question storage; qp_TextQuestionData is very small, thus kept in the same file;
386408 'model/qp_questiondata.php' => array( 'qp_QuestionData', 'qp_TextQuestionData' ),
387409 # collection of the questions
@@ -553,6 +575,7 @@
554576 global $qp_enable_showresults; // deprecated since v0.6.5
555577 global $qp_AnonForwardedFor; // deprecated since v0.6.5
556578 global $wgUser;
 579+ self::$output = $output;
557580 self::$article = $article;
558581 self::$title = $title;
559582 # in MW v1.15 / v1.16 user object was stub;
@@ -566,6 +589,7 @@
567590 $user = $wgUser;
568591 }
569592 self::$user = $user;
 593+ self::$request = $request;
570594 if ( isset( $qp_AnonForwardedFor ) ) {
571595 self::$anon_forwarded_for = $qp_AnonForwardedFor;
572596 }
@@ -619,8 +643,8 @@
620644 }
621645 global $wgQPollFunctionsHook;
622646 # setup tag hook
623 - $parser->setHook( self::$pollTag, array( 'qp_Setup', 'showPoll' ) );
624 - $parser->setHook( self::$interpTag, array( 'qp_Setup', 'showScript' ) );
 647+ $parser->setHook( self::$pollTag, array( __CLASS__, 'showPoll' ) );
 648+ $parser->setHook( self::$interpTag, array( __CLASS__, 'showScript' ) );
625649 $wgQPollFunctionsHook = new qp_FunctionsHook();
626650 # setup function hook
627651 $parser->setFunctionHook( 'qpuserchoice', array( &$wgQPollFunctionsHook, 'qpuserchoice' ), SFH_OBJECT_ARGS );
Index: trunk/extensions/QPoll/includes/qp_functionshook.php
@@ -91,7 +91,7 @@
9292 if ( preg_match( qp_Setup::PREG_POSITIVE_INT4_MATCH, $this->question_id ) ) {
9393 $this->question_id = intval( $this->question_id );
9494 $this->pollStore->loadQuestions();
95 - $this->pollStore->setLastUser( qp_Setup::getCurrUserName(), false );
 95+ $this->pollStore->setLastUser( qp_Setup::getCurrUserName() );
9696 $this->pollStore->loadUserVote();
9797 $this->error_message = 'missing_question_id';
9898 if ( array_key_exists( $this->question_id, $this->pollStore->Questions ) ) {
Index: trunk/extensions/QPoll/includes/qp_xlswriter.php
@@ -64,7 +64,9 @@
6565 static $wb;
6666 # an instance of XLS worksheet (only one currently is used)
6767 static $ws;
68 - # list of formats added to workbook
 68+ # list of format definitions added to workbook
 69+ static $fdef;
 70+ # list of format instances added to workbook
6971 static $format;
7072 # current row number in a worksheet (pointer)
7173 static $rownum = 0;
@@ -92,10 +94,15 @@
9395 */
9496 function addFormats( $formats ) {
9597 foreach ( $formats as $fkey => $fdef ) {
 98+ self::$fdef[$fkey] = $fdef;
9699 self::$format[$fkey] = self::$wb->addformat( $fdef );
97100 }
98101 }
99102
 103+ function getFormatDefinition( $fkey ) {
 104+ return self::$fdef[$fkey];
 105+ }
 106+
100107 function getFormat( $fkey ) {
101108 return self::$format[$fkey];
102109 }
Index: trunk/extensions/QPoll/interpretation/qp_interpret.php
@@ -109,8 +109,10 @@
110110 if ( !is_array( $result ) ) {
111111 return $interpResult->setError( wfMsg( 'qp_error_interpretation_no_return' ) );
112112 }
113 - if ( isset( $result['options'] ) && $result['options'] === 'noerrorstorage' ) {
114 - $interpResult->storeErroneous = false;
 113+ if ( isset( $result['options'] ) &&
 114+ is_array( $result['options'] ) &&
 115+ array_key_exists( 'store_erroneous', $result['options'] ) ) {
 116+ $interpResult->storeErroneous = (boolean) $result['options']['store_erroneous'];
115117 }
116118 if ( isset( $result['error'] ) && is_array( $result['error'] ) ) {
117119 # initialize $interpResult->qpcErrors[] member array
Index: trunk/extensions/QPoll/view/proposal/qp_tabularquestionproposalview.php
@@ -164,7 +164,7 @@
165165 # end of new span
166166 if ( $this->ctrl->poll->mBeingCorrected &&
167167 !$spanState->wasChecked &&
168 - $this->ctrl->mRequest->getVal( $name ) != $value ) {
 168+ qp_Setup::$request->getVal( $name ) != $value ) {
169169 # the span (a part of proposal) was submitted but unanswered
170170 $this->prependErrorMessage( wfMsg( 'qp_error_unanswered_span' ), 'NA' );
171171 # highlight current span to indicate an error
Index: trunk/extensions/QPoll/view/poll/qp_pollview.php
@@ -91,7 +91,6 @@
9292 * @return rendered "final" html
9393 */
9494 function renderPoll() {
95 - global $wgOut, $wgRequest;
9695 $pollStore = $this->ctrl->pollStore;
9796 # Generates the output.
9897 $qpoll_div = array( '__tag' => 'div', 'class' => 'qpoll' );
@@ -140,7 +139,8 @@
141140 $submitBtn[ 'disabled' ] = 'disabled';
142141 }
143142 # disable submit button in preview mode & printable version
144 - if ( $wgRequest->getVal( 'action' ) == 'parse' || $wgOut->isPrintable() ) {
 143+ if ( qp_Setup::$request->getVal( 'action' ) == 'parse' ||
 144+ qp_Setup::$output->isPrintable() ) {
145145 $submitBtn[ 'disabled' ] = 'disabled';
146146 }
147147 $submitBtn[ 'value' ] = wfMsgHtml( $submitMsg );
Index: trunk/extensions/QPoll/view/xls/qp_xlspoll.php
@@ -145,7 +145,7 @@
146146 if ( isset( $line['keys'] ) ) {
147147 # current node is associative array
148148 $this->writeRowLn( 0, $line['keys'], 'odd' );
149 - $ws->writeRowLn( 0, $line['vals'] );
 149+ $this->writeRowLn( 0, $line['vals'] );
150150 } else {
151151 $this->writeLn( 0, $line['vals'] );
152152 }
Index: trunk/extensions/QPoll/view/xls/qp_xlstextquestion.php
@@ -4,9 +4,10 @@
55
66 function __construct( $xls_fname = null ) {
77 parent::__construct( $xls_fname );
 8+ # answered categories will be displayed with already added format 'answer'
89 $this->addFormats( array(
9 - 'cat_part' => array( 'fgcolor' => 36, 'border' => 1 ),
10 - 'prop_part' => array( 'fgcolor' => 34, 'border' => 1 ),
 10+ 'cat_noanswer' => array( 'fgcolor' => 21, 'border' => 1 ),
 11+ 'prop_part' => array( 'fgcolor' => 27, 'border' => 1 ),
1112 ) );
1213 }
1314
@@ -46,7 +47,7 @@
4748 if ( !array_key_exists( $rowNum, $voicesTable ) ) {
4849 $voicesTable[$rowNum] = array();
4950 }
50 - $voicesTable[$rowNum++][$rowCol] = array( $option, 'format' => 'cat_part' );
 51+ $voicesTable[$rowNum++][$rowCol] = array( $option, 'format' => 'answer' );
5152 }
5253 $rowCol++;
5354 if ( ( $rowNum - $saveRowNum ) > $rowHeight ) {
@@ -54,15 +55,15 @@
5556 }
5657 $rowNum = $saveRowNum;
5758 } else {
58 - $voicesTable[$rowNum][$rowCol++] = array( array_pop( $selected_options ), 'format' => 'cat_part' );
 59+ $voicesTable[$rowNum][$rowCol++] = array( array_pop( $selected_options ), 'format' => 'answer' );
5960 }
6061 } else {
6162 # checkbox or radiobutton
62 - $voicesTable[$rowNum][$rowCol++] = array( qp_Setup::RESULTS_CHECK_SIGN, 'format' => 'cat_part' );
 63+ $voicesTable[$rowNum][$rowCol++] = array( qp_Setup::RESULTS_CHECK_SIGN, 'format' => 'answer' );
6364 }
6465 } else {
6566 # non-selected category (it has no selected option)
66 - $voicesTable[$rowNum][$rowCol++] = array( '', 'format' => 'cat_part' );
 67+ $voicesTable[$rowNum][$rowCol++] = array( '', 'format' => 'cat_noanswer' );
6768 }
6869 $catId++;
6970 } else {

Comments

#Comment by QuestPC (talk | contribs)   11:19, 16 November 2011

I don't have any experience with LoadBalancer, not sure whether my Database-related code will work with multiple master / slave servers correctly. I hope so, at least. Memory cache support was quite throughly tested.

Status & tagging log