Index: trunk/extensions/QPoll/i18n/qp.i18n.php |
— | — | @@ -124,6 +124,7 @@ |
125 | 125 | 'qp_error_too_long_category_option_value' => 'Category option value is too long to be stored in the database.', |
126 | 126 | 'qp_error_too_long_category_options_values' => 'Category options values are too long to be stored in the database.', |
127 | 127 | '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.', |
128 | 129 | 'qp_error_too_few_categories' => 'At least two categories must be defined.', |
129 | 130 | 'qp_error_too_few_spans' => 'Every category group must contain at least two subcategories.', |
130 | 131 | 'qp_error_no_answer' => 'Unanswered proposal.', |
— | — | @@ -238,7 +239,8 @@ |
239 | 240 | 'qp_error_too_many_spans' => 'There cannot be more category groups defined than the total count of subcategories.', |
240 | 241 | '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.', |
241 | 242 | '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.", |
243 | 245 | 'qp_error_too_few_spans' => 'Every category group should include at least two subcategories', |
244 | 246 | '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.', |
245 | 247 | '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 |
1 | 325 | + 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 |
1 | 97 | + 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 |
1 | 49 | + 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 |
1 | 50 | + native |
Index: trunk/extensions/QPoll/model/qp_questiondata.php |
— | — | @@ -5,71 +5,98 @@ |
6 | 6 | } |
7 | 7 | |
8 | 8 | /** |
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. |
10 | 13 | * ( instances usually have short name qdata ) |
11 | 14 | * |
12 | 15 | * *** Please do not instantiate directly. *** |
13 | | - * *** use qp_PollStore::newQuestionData() instead *** |
| 16 | + * *** use qp_QuestionData::factory() instead *** |
14 | 17 | * |
15 | 18 | */ |
16 | 19 | class qp_QuestionData { |
17 | 20 | |
18 | | - # associated view instance (singleton) |
| 21 | + ## associated view instance (singleton) |
19 | 22 | static protected $view; |
20 | 23 | |
21 | | - // DB index (with current scheme is non-unique) |
| 24 | + ## DB index (with current scheme is non-unique) |
22 | 25 | var $question_id = null; |
23 | | - // common properties |
| 26 | + ## common properties |
24 | 27 | var $type; |
25 | 28 | var $CommonQuestion; |
26 | 29 | var $Categories; |
27 | 30 | var $CategorySpans; |
28 | 31 | var $ProposalText; |
| 32 | + # since v0.8.0a, proposals may be addressed by their names |
| 33 | + # in the interpretation scripts |
29 | 34 | var $ProposalNames = array(); |
30 | 35 | var $ProposalCategoryId; |
31 | 36 | var $ProposalCategoryText; |
32 | 37 | 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] |
34 | 40 | var $Votes = null; |
| 41 | + # 3d array with floating point percent values for each [proposal][category], |
| 42 | + # calculated from $this->Votes |
35 | 43 | var $Percents = null; |
36 | 44 | |
37 | 45 | /** |
38 | 46 | * 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; |
43 | 51 | */ |
44 | 52 | function __construct( $argv ) { |
45 | 53 | 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; |
69 | 77 | } |
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__ ); |
71 | 79 | } |
72 | 80 | |
73 | 81 | /** |
| 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 | + /** |
74 | 101 | * Get appropriate view for Special:Pollresults |
75 | 102 | */ |
76 | 103 | function getView() { |
— | — | @@ -82,6 +109,28 @@ |
83 | 110 | } |
84 | 111 | |
85 | 112 | /** |
| 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 | + /** |
86 | 135 | * Integrate spans into categories |
87 | 136 | */ |
88 | 137 | function packSpans() { |
— | — | @@ -124,34 +173,15 @@ |
125 | 174 | } |
126 | 175 | |
127 | 176 | /** |
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 | | - /** |
150 | 177 | * Split raw proposal text from source page text or from DB |
151 | 178 | * into name part / text part |
152 | 179 | * |
153 | 180 | * @param $proposal_text string raw proposal text |
154 | 181 | * @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 |
156 | 186 | */ |
157 | 187 | static function splitRawProposal( &$proposal_text ) { |
158 | 188 | $matches = array(); |
— | — | @@ -159,13 +189,16 @@ |
160 | 190 | preg_match( '`^:\|(.+?)\|\s*(.+?)$`u', $proposal_text, $matches ); |
161 | 191 | if ( count( $matches ) > 2 ) { |
162 | 192 | if ( ( $prop_name = trim( $matches[1] ) ) !== '' ) { |
| 193 | + if ( strlen( $prop_name ) >= qp_Setup::$field_max_len['proposal_text'] ) { |
| 194 | + return false; |
| 195 | + } |
163 | 196 | # proposal name must be non-empty |
164 | 197 | $proposal_text = trim( $matches[2] ); |
165 | 198 | } |
166 | 199 | } |
167 | 200 | return $prop_name; |
168 | 201 | } |
169 | | - |
| 202 | + |
170 | 203 | /** |
171 | 204 | * Return proposal name prefix to be stored in DB (if any) |
172 | 205 | */ |
— | — | @@ -173,12 +206,82 @@ |
174 | 207 | return ( $name !== '' ) ? ":|{$name}|" : ''; |
175 | 208 | } |
176 | 209 | |
| 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 | + |
177 | 280 | } /* end of qp_QuestionData class */ |
178 | 281 | |
179 | 282 | /** |
180 | 283 | * |
181 | 284 | * *** Please do not instantiate directly. *** |
182 | | - * *** use qp_PollStore::newQuestionData() instead *** |
| 285 | + * *** use qp_QuestionData::factory() instead *** |
183 | 286 | * |
184 | 287 | */ |
185 | 288 | class qp_TextQuestionData extends qp_QuestionData { |
Index: trunk/extensions/QPoll/model/qp_pollstore.php |
— | — | @@ -5,29 +5,34 @@ |
6 | 6 | } |
7 | 7 | |
8 | 8 | /** |
9 | | - * poll storage and retrieval using DB |
| 9 | + * Poll storage and retrieval using DB |
10 | 10 | * 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. |
11 | 15 | */ |
12 | 16 | class qp_PollStore { |
13 | 17 | |
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 |
20 | 19 | var $pid = null; |
21 | 20 | var $last_uid = null; |
22 | 21 | |
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); |
24 | 24 | # also used by randomizer |
| 25 | + # For anonymous users it might be different from MediaWiki username, |
| 26 | + # because anonymous users can vote in qpoll. |
25 | 27 | var $username = ''; |
26 | 28 | |
27 | 29 | /*** common properties ***/ |
| 30 | + # article_id of wiki page where the poll is located |
28 | 31 | 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 |
30 | 35 | var $mPollId = null; |
31 | | - # order of poll on the page |
| 36 | + # order of definition of the poll in the source of the wiki page |
32 | 37 | var $mOrderId = null; |
33 | 38 | |
34 | 39 | /*** optional attributes ***/ |
— | — | @@ -39,14 +44,14 @@ |
40 | 45 | # '' indicates that interpretation template does not exists (a poll without quiz) |
41 | 46 | # null indicates that value is unknown (uninitialized yet) |
42 | 47 | var $interpDBkey = null; |
43 | | - # interpretation of user answer |
| 48 | + # interpretation of user answer (instance of qp_InterpResult) |
44 | 49 | var $interpResult; |
45 | 50 | # 1..n - number of random indexes from poll's header; 0 - poll questions are not randomized |
46 | 51 | # pollstore loads / saves random indexes for every user only when this property is NOT zero |
47 | 52 | # which improves performance of non-randomized polls |
48 | 53 | var $randomQuestionCount = null; |
49 | 54 | |
50 | | - # array of QuestionData instances (data from/to DB) |
| 55 | + # array of qp_QuestionData instances (question data convertation from / to DB) |
51 | 56 | var $Questions = null; |
52 | 57 | # array of random indexes of Questions[] array (optional) |
53 | 58 | var $randomQuestions = false; |
— | — | @@ -54,6 +59,7 @@ |
55 | 60 | # attempts of voting (passing the quiz). number of resubmits |
56 | 61 | # note: resubmits are counted for syntax-correct answer (when the vote is stored), |
57 | 62 | # yet the answer still might be logically incorrect (quiz is not passed / partially passed) |
| 63 | + # that depends on the value $this->interpResult->storeErroneous |
58 | 64 | var $attempts = 0; |
59 | 65 | |
60 | 66 | # poll processing state, read with getState() |
— | — | @@ -75,16 +81,17 @@ |
76 | 82 | # true, after the poll results have been successfully stored to DB |
77 | 83 | var $voteDone = false; |
78 | 84 | |
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 | + */ |
81 | 90 | function __construct( $argv = null ) { |
82 | | - global $wgParser; |
83 | | - if ( self::$db == null ) { |
84 | | - self::$db = & wfGetDB( DB_MASTER ); |
85 | | - } |
86 | 91 | $this->interpResult = new qp_InterpResult(); |
| 92 | + # set poll store of poll descriptions cache and all it's ancestors |
| 93 | + qp_PollCache::setStore( $this ); |
87 | 94 | if ( is_array( $argv ) && array_key_exists( "from", $argv ) ) { |
88 | | - $this->Questions = Array(); |
| 95 | + $this->Questions = array(); |
89 | 96 | $this->mCompletedPostData = 'NA'; |
90 | 97 | $this->pid = null; |
91 | 98 | $is_post = false; |
— | — | @@ -92,73 +99,12 @@ |
93 | 100 | case 'poll_post' : |
94 | 101 | $is_post = true; |
95 | 102 | 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 ); |
143 | 104 | break; |
144 | 105 | case 'pid' : |
145 | 106 | if ( array_key_exists( 'pid', $argv ) ) { |
146 | 107 | $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 ); |
163 | 109 | } |
164 | 110 | break; |
165 | 111 | default : |
— | — | @@ -167,15 +113,111 @@ |
168 | 114 | } |
169 | 115 | } |
170 | 116 | |
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 | + */ |
173 | 215 | static function newFromAddr( $pollAddr ) { |
174 | | - # build poll object from given poll address in args[0] |
| 216 | + # build poll object from given poll address |
175 | 217 | $pollAddr = qp_AbstractPoll::getPrefixedPollAddress( $pollAddr ); |
176 | 218 | if ( is_array( $pollAddr ) ) { |
177 | 219 | list( $pollTitleStr, $pollId ) = $pollAddr; |
178 | 220 | $pollTitle = Title::newFromURL( $pollTitleStr ); |
179 | | - if ( $pollTitle !== null ) { |
| 221 | + if ( $pollTitle instanceof Title ) { |
180 | 222 | $pollArticleId = intval( $pollTitle->getArticleID() ); |
181 | 223 | if ( $pollArticleId > 0 ) { |
182 | 224 | return new qp_PollStore( array( |
— | — | @@ -194,22 +236,9 @@ |
195 | 237 | } |
196 | 238 | |
197 | 239 | /** |
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 |
200 | 242 | */ |
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 | | - |
214 | 243 | function getPollId() { |
215 | 244 | return $this->mPollId; |
216 | 245 | } |
— | — | @@ -234,7 +263,11 @@ |
235 | 264 | !is_null ( $this->randomQuestionCount ); |
236 | 265 | } |
237 | 266 | |
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 | + */ |
239 | 272 | function getTitle() { |
240 | 273 | if ( $this->mArticleId === 0 ) { |
241 | 274 | throw new MWException( __METHOD__ . ' cannot be called for unsaved new pages' ); |
— | — | @@ -246,10 +279,10 @@ |
247 | 280 | throw new MWException( 'Unknown poll id in ' . __METHOD__ ); |
248 | 281 | } |
249 | 282 | $res = Title::newFromID( $this->mArticleId ); |
250 | | - $res->setFragment( qp_AbstractPoll::s_getPollTitleFragment( $this->mPollId ) ); |
251 | 283 | if ( !( $res instanceof Title ) ) { |
252 | | - throw new MWException( 'Invalid title created in ' . __METHOD__ ); |
| 284 | + throw new MWException( 'Cannot create poll title in ' . __METHOD__ ); |
253 | 285 | } |
| 286 | + $res->setFragment( qp_AbstractPoll::s_getPollTitleFragment( $this->mPollId ) ); |
254 | 287 | return $res; |
255 | 288 | } |
256 | 289 | |
— | — | @@ -269,10 +302,18 @@ |
270 | 303 | return ( $title instanceof Title ) ? $title : null; |
271 | 304 | } |
272 | 305 | |
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 | + */ |
274 | 315 | function isAlreadyVoted() { |
275 | 316 | if ( is_array( $this->Questions ) && count( $this->Questions > 0 ) ) { |
276 | | - foreach ( $this->Questions as &$qdata ) { |
| 317 | + foreach ( $this->Questions as $qdata ) { |
277 | 318 | if ( $qdata->alreadyVoted ) |
278 | 319 | return true; |
279 | 320 | } |
— | — | @@ -280,14 +321,22 @@ |
281 | 322 | return false; |
282 | 323 | } |
283 | 324 | |
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 | + */ |
286 | 329 | function questionExists( $question_id ) { |
287 | 330 | return array_key_exists( $question_id, $this->Questions ); |
288 | 331 | } |
289 | 332 | |
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 | + */ |
292 | 341 | function loadQuestions() { |
293 | 342 | $result = false; |
294 | 343 | $typeFromVer0_5 = array( |
— | — | @@ -295,25 +344,23 @@ |
296 | 345 | "multipleChoicePoll" => "multipleChoice", |
297 | 346 | "mixedChoicePoll" => "mixedChoice" |
298 | 347 | ); |
| 348 | + $db = wfGetDB( DB_MASTER ); |
299 | 349 | 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 ) { |
305 | 352 | $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; |
308 | 355 | # convert old (v0.5) question type string to the "new" type string |
309 | 356 | if ( isset( $typeFromVer0_5[$row->type] ) ) { |
310 | 357 | $row->type = $typeFromVer0_5[$row->type]; |
311 | 358 | } |
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( |
315 | 361 | 'qid' => $question_id, |
316 | 362 | 'type' => $row->type, |
317 | | - 'common_question' => $row->common_question ) ); |
| 363 | + 'common_question' => $row->common_question ) |
| 364 | + ); |
318 | 365 | } |
319 | 366 | $this->getCategories(); |
320 | 367 | $this->getProposalText(); |
— | — | @@ -323,25 +370,74 @@ |
324 | 371 | } |
325 | 372 | |
326 | 373 | /** |
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 |
331 | 376 | */ |
| 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 | + */ |
332 | 424 | function pollVotersPager( $offset = 0, $limit = 20 ) { |
333 | 425 | if ( $this->pid === null ) { |
334 | 426 | return false; |
335 | 427 | } |
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 | + ); |
344 | 440 | $result = array(); |
345 | | - while ( $row = self::$db->fetchObject( $res ) ) { |
| 441 | + while ( $row = $db->fetchObject( $res ) ) { |
346 | 442 | $interpResult = new qp_InterpResult(); |
347 | 443 | $interpResult->short = $row->short_interpretation; |
348 | 444 | $interpResult->long = $row->long_interpretation; |
— | — | @@ -355,23 +451,29 @@ |
356 | 452 | } |
357 | 453 | |
358 | 454 | /** |
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 |
363 | 459 | */ |
364 | | - function questionVoicesRange( $question_id, array $uids ) { |
| 460 | + function questionVoicesRange( $question_id, $uids ) { |
365 | 461 | if ( $this->pid === null ) { |
366 | 462 | return false; |
367 | 463 | } |
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 | + ); |
374 | 476 | $result = array(); |
375 | | - while ( $row = self::$db->fetchObject( $res ) ) { |
| 477 | + while ( $row = $db->fetchObject( $res ) ) { |
376 | 478 | $uid = intval( $row->uid ); |
377 | 479 | if ( !isset( $result[$uid] ) ) { |
378 | 480 | $result[$uid] = array(); |
— | — | @@ -380,29 +482,46 @@ |
381 | 483 | if ( !isset( $result[$uid][$proposal_id] ) ) { |
382 | 484 | $result[$uid][$proposal_id] = array(); |
383 | 485 | } |
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 ); |
385 | 487 | } |
386 | 488 | return $result; |
387 | 489 | } |
388 | 490 | |
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 | + */ |
393 | 511 | function loadUserAlreadyVoted() { |
394 | 512 | $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 ) { |
397 | 515 | return false; |
398 | 516 | } |
399 | | - $res = self::$db->select( 'qp_question_answers', |
| 517 | + $db = wfGetDB( DB_SLAVE ); |
| 518 | + $res = $db->select( 'qp_question_answers', |
400 | 519 | array( 'DISTINCT question_id' ), |
401 | 520 | array( 'pid' => $this->pid, 'uid' => $this->last_uid ), |
402 | 521 | __METHOD__ . ':load one user poll questions alreadyVoted values' ); |
403 | | - if ( self::$db->numRows( $res ) == 0 ) { |
| 522 | + if ( $db->numRows( $res ) == 0 ) { |
404 | 523 | return false; |
405 | 524 | } |
406 | | - while ( $row = self::$db->fetchObject( $res ) ) { |
| 525 | + while ( $row = $db->fetchObject( $res ) ) { |
407 | 526 | $question_id = intval( $row->question_id ); |
408 | 527 | if ( $this->questionExists( $question_id ) ) { |
409 | 528 | $result = $this->Questions[ $question_id ]->alreadyVoted = true; |
— | — | @@ -411,28 +530,36 @@ |
412 | 531 | return $result; |
413 | 532 | } |
414 | 533 | |
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 | + */ |
420 | 546 | function loadUserVote() { |
421 | 547 | $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 ) { |
424 | 550 | return false; |
425 | 551 | } |
426 | | - $res = self::$db->select( 'qp_question_answers', |
| 552 | + $db = wfGetDB( DB_SLAVE ); |
| 553 | + $res = $db->select( 'qp_question_answers', |
427 | 554 | array( 'question_id', 'proposal_id', 'cat_id', 'text_answer' ), |
428 | 555 | array( 'pid' => $this->pid, 'uid' => $this->last_uid ), |
429 | 556 | __METHOD__ . ':load one user single poll vote' ); |
430 | | - if ( self::$db->numRows( $res ) == 0 ) { |
| 557 | + if ( $db->numRows( $res ) == 0 ) { |
431 | 558 | return false; |
432 | 559 | } |
433 | | - while ( $row = self::$db->fetchObject( $res ) ) { |
| 560 | + while ( $row = $db->fetchObject( $res ) ) { |
434 | 561 | $question_id = intval( $row->question_id ); |
435 | 562 | if ( $this->questionExists( $question_id ) ) { |
436 | | - $qdata = &$this->Questions[ $question_id ]; |
| 563 | + $qdata = $this->Questions[$question_id]; |
437 | 564 | $result = $qdata->alreadyVoted = true; |
438 | 565 | $qdata->ProposalCategoryId[ intval( $row->proposal_id ) ][] = intval( $row->cat_id ); |
439 | 566 | $qdata->ProposalCategoryText[ intval( $row->proposal_id ) ][] = $row->text_answer; |
— | — | @@ -441,151 +568,73 @@ |
442 | 569 | return $result; |
443 | 570 | } |
444 | 571 | |
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 | + */ |
448 | 581 | 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 ); |
452 | 585 | 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; |
464 | 587 | } |
465 | | - $res = self::$db->select( 'qp_question_answers', |
| 588 | + $res = $db->select( 'qp_question_answers', |
466 | 589 | array( 'count(uid)', 'question_id', 'proposal_id', 'cat_id' ), |
467 | 590 | $where, |
468 | 591 | __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'] ); |
474 | 598 | 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)'] ) ); |
483 | 600 | } |
484 | 601 | } |
485 | 602 | } |
486 | 603 | } |
487 | 604 | |
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 ) { |
489 | 611 | $result = 0; |
| 612 | + $db = wfGetDB( DB_SLAVE ); |
490 | 613 | if ( $this->pid !== null ) { |
491 | | - $res = self::$db->select( 'qp_question_answers', |
| 614 | + $res = $db->select( 'qp_question_answers', |
492 | 615 | array( 'count(distinct uid)' ), |
493 | 616 | array( 'pid' => $this->pid, 'question_id' => $qdata->question_id ), |
494 | 617 | __METHOD__ ); |
495 | | - if ( $row = self::$db->fetchRow( $res ) ) { |
| 618 | + if ( $row = $db->fetchRow( $res ) ) { |
496 | 619 | $result = intval( $row[ "count(distinct uid)" ] ); |
497 | 620 | } |
498 | 621 | } |
499 | 622 | return $result; |
500 | 623 | } |
501 | 624 | |
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 | + */ |
503 | 629 | function calculateStatistics() { |
504 | | - foreach ( $this->Questions as &$qdata ) { |
505 | | - $this->calculateQuestionStatistics( $qdata ); |
| 630 | + foreach ( $this->Questions as $qdata ) { |
| 631 | + $qdata->calculateQuestionStatistics( $this ); |
506 | 632 | } |
507 | 633 | } |
508 | 634 | |
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 | + */ |
590 | 639 | function getState() { |
591 | 640 | return $this->mCompletedPostData; |
592 | 641 | } |
— | — | @@ -600,16 +649,18 @@ |
601 | 650 | $this->mCompletedPostData = 'error'; |
602 | 651 | } |
603 | 652 | |
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 | + */ |
606 | 657 | 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() ) { |
609 | 661 | $this->mCompletedPostData = 'complete'; |
610 | 662 | return true; |
611 | | - } else { |
612 | | - return false; |
613 | 663 | } |
| 664 | + return false; |
614 | 665 | } |
615 | 666 | |
616 | 667 | /** |
— | — | @@ -624,23 +675,30 @@ |
625 | 676 | } |
626 | 677 | |
627 | 678 | /** |
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. |
630 | 681 | */ |
631 | 682 | function loadRandomQuestions() { |
632 | 683 | if ( $this->mArticleId == 0 ) { |
633 | 684 | $this->randomQuestions = false; |
634 | 685 | return; |
635 | 686 | } |
636 | | - if ( is_null( $this->pid ) ) { |
| 687 | + if ( $this->pid === null ) { |
637 | 688 | throw new MWException( __METHOD__ . ' cannot be called when pid was not set' ); |
638 | 689 | } |
639 | | - if ( is_null( $this->last_uid ) ) { |
| 690 | + if ( $this->last_uid === null ) { |
640 | 691 | throw new MWException( __METHOD__ . ' cannot be called when uid was not set' ); |
641 | 692 | } |
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 | + ); |
643 | 701 | $this->randomQuestions = array(); |
644 | | - while ( $row = self::$db->fetchObject( $res ) ) { |
| 702 | + while ( $row = $db->fetchObject( $res ) ) { |
645 | 703 | $this->randomQuestions[] = intval( $row->question_id ); |
646 | 704 | } |
647 | 705 | if ( count( $this->randomQuestions ) === 0 ) { |
— | — | @@ -653,42 +711,53 @@ |
654 | 712 | /** |
655 | 713 | * Stores $this->randomQuestions into DB |
656 | 714 | * 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; |
658 | 716 | * when number of random questions for poll was changed |
659 | 717 | */ |
660 | 718 | function setRandomQuestions() { |
661 | 719 | if ( $this->mArticleId == 0 ) { |
662 | 720 | return; |
663 | 721 | } |
664 | | - if ( is_null( $this->pid ) ) { |
| 722 | + if ( $this->pid === null ) { |
665 | 723 | throw new MWException( __METHOD__ . ' cannot be called when pid was not set' ); |
666 | 724 | } |
667 | | - if ( is_null( $this->last_uid ) ) { |
| 725 | + if ( $this->last_uid === null ) { |
668 | 726 | throw new MWException( __METHOD__ . ' cannot be called when uid was not set' ); |
669 | 727 | } |
| 728 | + $db = wfGetDB( DB_MASTER ); |
670 | 729 | if ( is_array( $this->randomQuestions ) ) { |
671 | 730 | $data = array(); |
672 | 731 | foreach ( $this->randomQuestions as $qidx ) { |
673 | 732 | $data[] = array( 'pid' => $this->pid, 'uid' => $this->last_uid, 'question_id' => $qidx ); |
674 | 733 | } |
675 | | - self::$db->begin(); |
676 | | - self::$db->delete( 'qp_random_questions', |
| 734 | + $db->begin(); |
| 735 | + $db->delete( 'qp_random_questions', |
677 | 736 | 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', |
680 | 740 | $data, |
681 | | - __METHOD__ . ':set random questions seed' ); |
682 | | - self::$db->commit(); |
| 741 | + __METHOD__ . ':set random questions seed' |
| 742 | + ); |
| 743 | + $db->commit(); |
683 | 744 | return; |
684 | 745 | } |
685 | 746 | # this->randomQuestions === false; this poll is not randomized anymore |
686 | | - self::$db->delete( 'qp_random_questions', |
| 747 | + $db->delete( 'qp_random_questions', |
687 | 748 | array( 'pid' => $this->pid, 'uid' => $this->last_uid ), |
688 | 749 | __METHOD__ . ':remove question random seed' |
689 | 750 | ); |
690 | 751 | } |
691 | 752 | |
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 ) { |
693 | 762 | if ( $this->pid === null ) { |
694 | 763 | return; |
695 | 764 | } |
— | — | @@ -696,30 +765,27 @@ |
697 | 766 | if ( $this->username === $username ) { |
698 | 767 | return; |
699 | 768 | } |
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 ); |
702 | 774 | 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; |
716 | 778 | } |
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', |
718 | 783 | array( 'attempts', 'short_interpretation', 'long_interpretation', 'structured_interpretation' ), |
719 | 784 | 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 ); |
724 | 790 | $this->interpResult = new qp_InterpResult(); |
725 | 791 | $this->interpResult->short = $row->short_interpretation; |
726 | 792 | $this->interpResult->long = $row->long_interpretation; |
— | — | @@ -732,158 +798,131 @@ |
733 | 799 | // todo: change to "insert ... on duplicate key update ..." when last_insert_id() bugs will be fixed |
734 | 800 | } |
735 | 801 | |
| 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 | + */ |
736 | 828 | function getUserName( $uid ) { |
| 829 | + $db = wfGetDB( DB_MASTER ); |
737 | 830 | 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 ) { |
741 | 839 | return $row->name; |
742 | 840 | } |
743 | 841 | } |
744 | 842 | return false; |
745 | 843 | } |
746 | 844 | |
| 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 | + */ |
747 | 850 | private function loadPid() { |
748 | 851 | if ( $this->mArticleId === 0 ) { |
749 | 852 | return; |
750 | 853 | } |
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 ) { |
757 | 856 | $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; |
759 | 860 | if ( $this->mOrderId === null ) { |
760 | 861 | $this->mOrderId = $row->order_id; |
| 862 | + $updates_counter++; |
761 | 863 | } |
762 | 864 | if ( $this->dependsOn === null ) { |
763 | 865 | $this->dependsOn = $row->dependance; |
| 866 | + $updates_counter++; |
764 | 867 | } |
765 | | - if ( is_null( $this->interpDBkey ) ) { |
| 868 | + if ( $this->interpDBkey === null ) { |
766 | 869 | $this->interpNS = $row->interpretation_namespace; |
767 | 870 | $this->interpDBkey = $row->interpretation_title; |
| 871 | + $updates_counter++; |
768 | 872 | } |
769 | | - if ( is_null( $this->randomQuestionCount ) ) { |
| 873 | + if ( $this->randomQuestionCount === null ) { |
770 | 874 | $this->randomQuestionCount = $row->random_question_count; |
| 875 | + $updates_counter++; |
771 | 876 | } |
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 | + } |
773 | 882 | } |
774 | 883 | } |
775 | 884 | |
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() { |
777 | 891 | if ( $this->mArticleId === 0 ) { |
778 | 892 | throw new MWException( 'Cannot save new poll description during new page preprocess in ' . __METHOD__ ); |
779 | 893 | } |
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; |
809 | 904 | } |
810 | 905 | |
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 ); |
823 | 922 | } |
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(); |
833 | 923 | } |
834 | 924 | |
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 | | - |
886 | 925 | /** |
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. |
888 | 927 | * Stores the result in $this->interpResult |
889 | 928 | */ |
890 | 929 | private function interpretVote() { |
— | — | @@ -903,7 +942,7 @@ |
904 | 943 | # prepare array of user answers that will be passed to the interpreter |
905 | 944 | $poll_answer = array(); |
906 | 945 | |
907 | | - foreach ( $this->Questions as &$qdata ) { |
| 946 | + foreach ( $this->Questions as $qdata ) { |
908 | 947 | if ( !$this->isUsedQuestion( $qdata->question_id ) ) { |
909 | 948 | continue; |
910 | 949 | } |
— | — | @@ -930,56 +969,79 @@ |
931 | 970 | $this->interpResult = qp_Interpret::getResult( $interpArticle, array( 'answer' => $poll_answer, 'randomQuestions' => $this->randomQuestions ) ); |
932 | 971 | } |
933 | 972 | |
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 ) { |
938 | 981 | foreach ( $ques->ProposalCategoryId as $propkey => &$prop_answers ) { |
939 | 982 | 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 | + ); |
941 | 991 | } |
942 | 992 | } |
943 | 993 | } |
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', |
947 | 997 | array( 'uid' => $this->last_uid, 'pid' => $this->pid ), |
948 | 998 | __METHOD__ . ':delete previous answers of current user to the same poll' |
949 | 999 | ); |
950 | 1000 | # vote |
951 | 1001 | if ( count( $insert ) > 0 ) { |
952 | | - self::$db->replace( 'qp_question_answers', |
| 1002 | + $db->replace( 'qp_question_answers', |
953 | 1003 | array( 'answer' ), |
954 | 1004 | $insert, |
955 | | - __METHOD__ ); |
| 1005 | + __METHOD__ |
| 1006 | + ); |
| 1007 | + $this->attempts++; |
956 | 1008 | # 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__ ); |
964 | 1022 | } |
965 | 1023 | } |
966 | 1024 | |
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 | + */ |
968 | 1030 | function setUserVote() { |
969 | | - if ( $this->pid !== null && |
| 1031 | + if ( $this->hasQuestions() && |
970 | 1032 | $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 | + ) { |
977 | 1035 | $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' ); |
978 | 1040 | if ( $this->interpResult->hasToBeStored() ) { |
979 | | - $this->setAnswers(); |
| 1041 | + $this->setAnswers( $db ); |
980 | 1042 | } |
981 | | - self::$db->commit(); |
| 1043 | + $db->commit(); |
982 | 1044 | $this->voteDone = true; |
983 | 1045 | } |
984 | 1046 | } |
985 | 1047 | |
986 | | -} |
| 1048 | +} /* enf of qp_PollStore class */ |
Index: trunk/extensions/QPoll/specials/qp_results.php |
— | — | @@ -209,7 +209,7 @@ |
210 | 210 | } |
211 | 211 | $userTitle = Title::makeTitleSafe( NS_USER, $userName ); |
212 | 212 | $user_link = $this->qpLink( $userTitle, $userName ); |
213 | | - $pollStore->setLastUser( $userName, false ); |
| 213 | + $pollStore->setLastUser( $userName ); |
214 | 214 | if ( !$pollStore->loadUserVote() ) { |
215 | 215 | return ''; |
216 | 216 | } |
— | — | @@ -244,7 +244,7 @@ |
245 | 245 | $poll_link = $this->qpLink( $poll_title, $poll_title->getPrefixedText() . wfMsg( 'word-separator' ) . wfMsg( 'qp_parentheses', $pollStore->mPollId ) ); |
246 | 246 | $output .= wfMsg( 'qp_browse_to_poll', $poll_link ) . "<br />\n"; |
247 | 247 | $interpTitle = $pollStore->getInterpTitle(); |
248 | | - if ( $interpTitle !== null ) { |
| 248 | + if ( $interpTitle instanceof Title ) { |
249 | 249 | $interp_link = $this->qpLink( $interpTitle, $interpTitle->getPrefixedText() ); |
250 | 250 | $output .= wfMsg( 'qp_browse_to_interpretation', $interp_link ) . "<br />\n"; |
251 | 251 | } |
— | — | @@ -293,7 +293,7 @@ |
294 | 294 | # statistics export uses additional formats |
295 | 295 | $percent_num_format = '[Blue]0.0%;[Red]-0.0%;[Black]0%'; |
296 | 296 | $qp_xls->addFormats( array( |
297 | | - 'percent' => array( 'fgcolor' => 0x1A, 'border' => 1 ) |
| 297 | + 'percent' => $qp_xls->getFormatDefinition( 'answer' ) |
298 | 298 | ) ); |
299 | 299 | $qp_xls->getFormat( 'percent' )->setAlign( 'left' ); |
300 | 300 | $qp_xls->getFormat( 'percent' )->setNumFormat( $percent_num_format ); |
— | — | @@ -354,17 +354,17 @@ |
355 | 355 | |
356 | 356 | function getIntervalResults( $offset, $limit ) { |
357 | 357 | $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' ), |
362 | 360 | array( 'qu.uid as uid', 'name as username', 'count(pid) as pidcount' ), |
363 | | - 'qu.uid=qup.uid', |
| 361 | + /* WHERE */ 'qu.uid=qup.uid', |
364 | 362 | __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 | + ) |
369 | 369 | ); |
370 | 370 | while ( $row = $db->fetchObject( $res ) ) { |
371 | 371 | $result[] = $row; |
— | — | @@ -418,12 +418,13 @@ |
419 | 419 | # fake pollStore to get username by uid: avoid to use this trick as much as possible |
420 | 420 | $pollStore = new qp_PollStore(); |
421 | 421 | $userName = $pollStore->getUserName( $this->uid ); |
422 | | - $db = & wfGetDB( DB_SLAVE ); |
| 422 | + $db = wfGetDB( DB_SLAVE ); |
423 | 423 | $res = $db->select( |
424 | | - array( 'qp_users_polls' ), |
| 424 | + 'qp_users_polls', |
425 | 425 | array( 'count(pid) as pidcount' ), |
426 | | - 'uid=' . $db->addQuotes( $this->uid ), |
427 | | - __METHOD__ ); |
| 426 | + /* WHERE */ array( 'uid' => $this->uid ), |
| 427 | + __METHOD__ |
| 428 | + ); |
428 | 429 | if ( $row = $db->fetchObject( $res ) ) { |
429 | 430 | $pidcount = $row->pidcount; |
430 | 431 | } else { |
— | — | @@ -438,18 +439,18 @@ |
439 | 440 | |
440 | 441 | function getIntervalResults( $offset, $limit ) { |
441 | 442 | $result = Array(); |
442 | | - $db = & wfGetDB( DB_SLAVE ); |
| 443 | + $db = wfGetDB( DB_SLAVE ); |
443 | 444 | $page = $db->tableName( 'page' ); |
444 | 445 | $qp_poll_desc = $db->tableName( 'qp_poll_desc' ); |
445 | 446 | $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 ); |
454 | 455 | $res = $db->query( $query, __METHOD__ ); |
455 | 456 | while ( $row = $db->fetchObject( $res ) ) { |
456 | 457 | $result[] = $row; |
— | — | @@ -485,15 +486,17 @@ |
486 | 487 | |
487 | 488 | function getIntervalResults( $offset, $limit ) { |
488 | 489 | $result = array(); |
489 | | - $db = & wfGetDB( DB_SLAVE ); |
| 490 | + $db = wfGetDB( DB_SLAVE ); |
490 | 491 | $res = $db->select( |
491 | 492 | array( 'page', 'qp_poll_desc' ), |
492 | 493 | 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', |
494 | 495 | __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 | + ) |
498 | 501 | ); |
499 | 502 | while ( $row = $db->fetchObject( $res ) ) { |
500 | 503 | $result[] = $row; |
— | — | @@ -539,12 +542,13 @@ |
540 | 543 | function getPageHeader() { |
541 | 544 | global $wgLang, $wgContLang; |
542 | 545 | $link = ""; |
543 | | - $db = & wfGetDB( DB_SLAVE ); |
| 546 | + $db = wfGetDB( DB_SLAVE ); |
544 | 547 | $res = $db->select( |
545 | 548 | array( 'page', 'qp_poll_desc' ), |
546 | 549 | 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 | + ); |
549 | 553 | if ( $row = $db->fetchObject( $res ) ) { |
550 | 554 | $poll_title = Title::makeTitle( intval( $row->ns ), $row->title, qp_AbstractPoll::s_getPollTitleFragment( $row->poll_id, '' ) ); |
551 | 555 | $pagename = qp_Setup::specialchars( $wgContLang->convert( $poll_title->getPrefixedText() ) ); |
— | — | @@ -562,15 +566,15 @@ |
563 | 567 | |
564 | 568 | function getIntervalResults( $offset, $limit ) { |
565 | 569 | $result = Array(); |
566 | | - $db = & wfGetDB( DB_SLAVE ); |
| 570 | + $db = wfGetDB( DB_SLAVE ); |
567 | 571 | $qp_users = $db->tableName( 'qp_users' ); |
568 | 572 | $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 ); |
575 | 579 | $res = $db->query( $query, __METHOD__ ); |
576 | 580 | while ( $row = $db->fetchObject( $res ) ) { |
577 | 581 | $result[] = $row; |
— | — | @@ -618,13 +622,12 @@ |
619 | 623 | $this->question_id = $question_id; |
620 | 624 | $this->proposal_id = $proposal_id; |
621 | 625 | $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 | + ); |
629 | 632 | if ( $row = $db->fetchObject( $res ) ) { |
630 | 633 | $this->pid = intval( $row->pid ); |
631 | 634 | $this->ns = intval( $row->ns ); |
— | — | @@ -636,7 +639,6 @@ |
637 | 640 | function getPageHeader() { |
638 | 641 | global $wgLang, $wgContLang; |
639 | 642 | $link = ""; |
640 | | - $db = & wfGetDB( DB_SLAVE ); |
641 | 643 | if ( $this->pid !== null ) { |
642 | 644 | $pollStore = new qp_PollStore( array( 'from' => 'pid', 'pid' => $this->pid ) ); |
643 | 645 | if ( $pollStore->pid !== null ) { |
— | — | @@ -683,15 +685,17 @@ |
684 | 686 | |
685 | 687 | function getIntervalResults( $offset, $limit ) { |
686 | 688 | $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 | + ); |
696 | 700 | while ( $row = $db->fetchObject( $res ) ) { |
697 | 701 | $result[] = $row; |
698 | 702 | } |
Index: trunk/extensions/QPoll/ctrl/poll/qp_abstractpoll.php |
— | — | @@ -100,9 +100,8 @@ |
101 | 101 | * @public |
102 | 102 | */ |
103 | 103 | 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(); |
107 | 106 | # Determine which messages will be used, according to the language. |
108 | 107 | qp_Setup::onLoadAllMessages(); |
109 | 108 | $view->setController( $this ); |
— | — | @@ -220,7 +219,6 @@ |
221 | 220 | // array[2] - prefixed (complete) poll address |
222 | 221 | // false - invalid source poll address was given |
223 | 222 | static function getPrefixedPollAddress( $addr ) { |
224 | | - global $wgTitle; |
225 | 223 | if ( is_array( $addr ) ) { |
226 | 224 | if ( count( $addr ) > 1 ) { |
227 | 225 | list( $titlePart, $pollIdPart ) = $addr; |
— | — | @@ -241,7 +239,7 @@ |
242 | 240 | } |
243 | 241 | if ( $titlePart == '' ) { |
244 | 242 | # poll is located at the current page |
245 | | - $titlePart = $wgTitle->getPrefixedText(); |
| 243 | + $titlePart = qp_Setup::$title->getPrefixedText(); |
246 | 244 | } |
247 | 245 | return array( $titlePart, $pollIdPart, $titlePart . '#' . $pollIdPart ); |
248 | 246 | } |
Index: trunk/extensions/QPoll/ctrl/poll/qp_pollstats.php |
— | — | @@ -78,7 +78,7 @@ |
79 | 79 | $this->mState = "error"; |
80 | 80 | return self::fatalErrorQuote( 'qp_error_no_stats', $this->pollAddr ); |
81 | 81 | } |
82 | | - $this->pollStore->setLastUser( $this->username, false ); |
| 82 | + $this->pollStore->setLastUser( $this->username ); |
83 | 83 | # do not check the result, because we may show results even if the user hasn't voted |
84 | 84 | $this->pollStore->loadUserAlreadyVoted(); |
85 | 85 | return true; |
Index: trunk/extensions/QPoll/ctrl/poll/qp_poll.php |
— | — | @@ -97,7 +97,7 @@ |
98 | 98 | # order_id is used to sort out polls on the Special:PollResults statistics page |
99 | 99 | $this->mOrderId = self::$sOrderId; |
100 | 100 | # 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 ); |
102 | 102 | } |
103 | 103 | |
104 | 104 | /** |
— | — | @@ -157,13 +157,13 @@ |
158 | 158 | $newPollStore['from'] = 'poll_post'; |
159 | 159 | $this->pollStore = new qp_PollStore( $newPollStore ); |
160 | 160 | $this->pollStore->loadQuestions(); |
161 | | - $this->pollStore->setLastUser( $this->username, false ); |
| 161 | + $this->pollStore->setLastUser( $this->username ); |
162 | 162 | $this->pollStore->loadUserAlreadyVoted(); |
163 | 163 | } else { |
164 | 164 | $newPollStore['from'] = 'poll_get'; |
165 | 165 | $this->pollStore = new qp_PollStore( $newPollStore ); |
166 | 166 | $this->pollStore->loadQuestions(); |
167 | | - $this->pollStore->setLastUser( $this->username, false ); |
| 167 | + $this->pollStore->setLastUser( $this->username ); |
168 | 168 | # to show previous choice of current user, if that's available |
169 | 169 | # do not check the result, because user can vote even if haven't voted before |
170 | 170 | $this->pollStore->loadUserVote(); |
— | — | @@ -204,8 +204,11 @@ |
205 | 205 | $this->questions->randomize( $this->randomQuestionCount ); |
206 | 206 | $this->pollStore->randomQuestions = $this->questions->getUsedQuestions(); |
207 | 207 | # 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 | + } |
210 | 213 | } |
211 | 214 | |
212 | 215 | /** |
— | — | @@ -215,7 +218,6 @@ |
216 | 219 | * @return boolean true - stop further processing, false - continue processing |
217 | 220 | */ |
218 | 221 | function parseInput( $input ) { |
219 | | - global $wgTitle; |
220 | 222 | # parse the input; generates $this->questions collection |
221 | 223 | $this->parseQuestionsHeaders( $input ); |
222 | 224 | $this->setUsedQuestions(); |
— | — | @@ -228,7 +230,7 @@ |
229 | 231 | } |
230 | 232 | if ( $this->pollStore->stateComplete() ) { |
231 | 233 | # store user vote to the DB (when the poll is fine) |
232 | | - $this->pollStore->setLastUser( $this->username ); |
| 234 | + $this->pollStore->createLastUser( $this->username ); |
233 | 235 | $this->pollStore->setUserVote(); |
234 | 236 | } |
235 | 237 | if ( $this->pollStore->interpResult->isError() ) { |
— | — | @@ -240,7 +242,7 @@ |
241 | 243 | $this->mResponse->setcookie( 'QPoll', 'clearCache', time() + 20 ); |
242 | 244 | } |
243 | 245 | $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() ); |
245 | 247 | return true; |
246 | 248 | } |
247 | 249 | return false; |
— | — | @@ -269,7 +271,7 @@ |
270 | 272 | if ( !$depPollStore->loadQuestions() ) { |
271 | 273 | return self::fatalErrorNoQuote( 'qp_error_vote_dependance_poll', $depLink ); |
272 | 274 | } |
273 | | - $depPollStore->setLastUser( $this->username, false ); |
| 275 | + $depPollStore->setLastUser( $this->username ); |
274 | 276 | if ( $depPollStore->loadUserAlreadyVoted() ) { |
275 | 277 | # user already voted in current the poll in chain |
276 | 278 | if ( $depPollStore->dependsOn === '' ) { |
— | — | @@ -471,7 +473,7 @@ |
472 | 474 | if ( $this->mBeingCorrected ) { |
473 | 475 | if ( $question->getState() == '' ) { |
474 | 476 | # question is OK, store it into pollStore |
475 | | - $question->store( $this->pollStore ); |
| 477 | + $this->pollStore->setQuestionAnswer( $question ); |
476 | 478 | } else { |
477 | 479 | # http post: not every proposals were answered: do not update DB |
478 | 480 | $this->pollStore->stateIncomplete(); |
Index: trunk/extensions/QPoll/ctrl/question/qp_abstractquestion.php |
— | — | @@ -45,8 +45,6 @@ |
46 | 46 | * @param $questionId the identifier of the question used to generate input names |
47 | 47 | */ |
48 | 48 | function __construct( qp_AbstractPoll $poll, qp_AbstractView $view, $questionId ) { |
49 | | - global $wgRequest; |
50 | | - $this->mRequest = &$wgRequest; |
51 | 49 | # the question collection is not sparce by default |
52 | 50 | $this->mQuestionId = $this->usedId = $questionId; |
53 | 51 | $view->setController( $this ); |
Index: trunk/extensions/QPoll/ctrl/question/qp_textquestion.php |
— | — | @@ -296,11 +296,11 @@ |
297 | 297 | $text_answer = ''; |
298 | 298 | # try to load from POST data |
299 | 299 | if ( $this->poll->mBeingCorrected && |
300 | | - ( $ta = $this->mRequest->getArray( $name ) ) !== null ) { |
| 300 | + ( $ta = qp_Setup::$request->getArray( $name ) ) !== null ) { |
301 | 301 | if ( $opt->type === 'text' ) { |
302 | 302 | if ( count( $ta ) === 1 ) { |
303 | 303 | # fallback to WebRequest::getText(), because it offers useful preprocessing |
304 | | - $ta = trim( $this->mRequest->getText( $name ) ); |
| 304 | + $ta = trim( qp_Setup::$request->getText( $name ) ); |
305 | 305 | } else { |
306 | 306 | # pack select multiple values |
307 | 307 | $ta = implode( qp_Setup::SELECT_MULTIPLE_VALUES_SEPARATOR, array_map( 'trim', $ta ) ); |
— | — | @@ -466,7 +466,12 @@ |
467 | 467 | $opt->reset(); |
468 | 468 | $this->propview = new qp_TextQuestionProposalView( $proposalId, $this ); |
469 | 469 | # 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 | + } |
471 | 476 | $this->dbtokens = $brace_stack = array(); |
472 | 477 | $dbtokens_idx = -1; |
473 | 478 | $catId = 0; |
Index: trunk/extensions/QPoll/ctrl/question/qp_mixedquestion.php |
— | — | @@ -43,7 +43,10 @@ |
44 | 44 | } |
45 | 45 | $proposalId++; |
46 | 46 | # 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 !== '' ) { |
48 | 51 | $this->mProposalNames[$proposalId] = $prop_name; |
49 | 52 | } |
50 | 53 | $this->mProposalText[$proposalId] = trim( $pview->text ); |
— | — | @@ -74,9 +77,9 @@ |
75 | 78 | # Determine if the input has to be checked. |
76 | 79 | $input_checked = false; |
77 | 80 | $text_answer = ''; |
78 | | - if ( $this->poll->mBeingCorrected && $this->mRequest->getVal( $name ) !== null ) { |
| 81 | + if ( $this->poll->mBeingCorrected && qp_Setup::$request->getVal( $name ) !== null ) { |
79 | 82 | if ( $inputType == 'text' ) { |
80 | | - $text_answer = trim( $this->mRequest->getText( $name ) ); |
| 83 | + $text_answer = trim( qp_Setup::$request->getText( $name ) ); |
81 | 84 | if ( strlen( $text_answer ) > qp_Setup::$field_max_len['text_answer'] ) { |
82 | 85 | $text_answer = $wgContLang->truncate( $text_answer, qp_Setup::$field_max_len['text_answer'] , '' ); |
83 | 86 | } |
Index: trunk/extensions/QPoll/ctrl/question/qp_tabularquestion.php |
— | — | @@ -282,7 +282,10 @@ |
283 | 283 | $proposalId++; |
284 | 284 | $pview->text = array_pop( $matches ); |
285 | 285 | # 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 !== '' ) { |
287 | 290 | $this->mProposalNames[$proposalId] = $prop_name; |
288 | 291 | } |
289 | 292 | $this->mProposalText[$proposalId] = trim( $pview->text ); |
— | — | @@ -305,7 +308,7 @@ |
306 | 309 | break; |
307 | 310 | } |
308 | 311 | # 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 ) { |
310 | 313 | $inp[ 'checked' ] = 'checked'; |
311 | 314 | } |
312 | 315 | if ( $this->answerExists( $inputType, $proposalId, $catId ) !== false ) { |
Index: trunk/extensions/QPoll/ctrl/question/qp_stubquestion.php |
— | — | @@ -85,25 +85,6 @@ |
86 | 86 | return false; |
87 | 87 | } |
88 | 88 | |
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 | | - |
108 | 89 | function isUniqueProposalCategoryId( $proposalId, $catId ) { |
109 | 90 | foreach ( $this->mProposalCategoryId as $proposalCategoryId ) { |
110 | 91 | if ( in_array( $catId, $proposalCategoryId ) ) { |
Index: trunk/extensions/QPoll/qp_user.php |
— | — | @@ -128,7 +128,7 @@ |
129 | 129 | } |
130 | 130 | $username = qp_Setup::getCurrUserName(); |
131 | 131 | $pollStore->loadQuestions(); |
132 | | - $pollStore->setLastUser( $username, false ); |
| 132 | + $pollStore->setLastUser( $username ); |
133 | 133 | if ( $pollStore->interpResult->structured === '' ) { |
134 | 134 | return null; |
135 | 135 | } |
— | — | @@ -141,10 +141,11 @@ |
142 | 142 | */ |
143 | 143 | class qp_Setup { |
144 | 144 | |
| 145 | + # internal unique error codes |
145 | 146 | const ERROR_MISSED_TITLE = 1; |
146 | 147 | const ERROR_INVALID_ADDRESS = 2; |
147 | 148 | |
148 | | - # unicode character used to display selected checkboxes and radiobuttons in |
| 149 | + # unicode entity used to display selected checkboxes and radiobuttons in |
149 | 150 | # result views at Special:Pollresults page |
150 | 151 | const RESULTS_CHECK_SIGN = '☆'; |
151 | 152 | |
— | — | @@ -168,10 +169,14 @@ |
169 | 170 | static $ExtDir; // filesys path with windows path fix |
170 | 171 | static $ScriptPath; // apache virtual path |
171 | 172 | 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 |
175 | 173 | |
| 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 | + |
176 | 181 | /** |
177 | 182 | * The map of question 'type' attribute value to the question's ctrl / view / subtype. |
178 | 183 | */ |
— | — | @@ -274,6 +279,11 @@ |
275 | 280 | ); |
276 | 281 | /* end of default configuration settings */ |
277 | 282 | |
| 283 | + # unicode character used to display selected checkboxes and radiobuttons in |
| 284 | + # result views at Special:Pollresults page |
| 285 | + static $resultsCheckCode = '+'; |
| 286 | + |
| 287 | + |
278 | 288 | static function entities( $s ) { |
279 | 289 | return htmlentities( $s, ENT_QUOTES, 'UTF-8' ); |
280 | 290 | } |
— | — | @@ -282,6 +292,10 @@ |
283 | 293 | return htmlentities( $s, ENT_QUOTES, 'UTF-8' ); |
284 | 294 | } |
285 | 295 | |
| 296 | + static function entity_decode( $s ) { |
| 297 | + return html_entity_decode( $s, ENT_QUOTES, 'UTF-8' ); |
| 298 | + } |
| 299 | + |
286 | 300 | /** |
287 | 301 | * Autoload classes from the map provided |
288 | 302 | */ |
— | — | @@ -316,6 +330,8 @@ |
317 | 331 | $top_dir = array_pop( $dirs ); |
318 | 332 | self::$ScriptPath = $wgScriptPath . '/extensions' . ( ( $top_dir == 'extensions' ) ? '' : '/' . $top_dir ); |
319 | 333 | |
| 334 | + self::$resultsCheckCode = self::entity_decode( self::RESULTS_CHECK_SIGN ); |
| 335 | + |
320 | 336 | # language files |
321 | 337 | # extension messages |
322 | 338 | $wgExtensionMessagesFiles['QPoll'] = self::$ExtDir . '/i18n/qp.i18n.php'; |
— | — | @@ -326,7 +342,7 @@ |
327 | 343 | |
328 | 344 | # extension setup, hooks handling and content transformation |
329 | 345 | self::autoLoad( array( |
330 | | - 'qp_user.php' => 'qp_Setup', |
| 346 | + 'qp_user.php' => __CLASS__, |
331 | 347 | 'includes/qp_functionshook.php' => 'qp_FunctionsHook', |
332 | 348 | 'includes/qp_renderer.php' => 'qp_Renderer', |
333 | 349 | 'includes/qp_xlswriter.php' => 'qp_XlsWriter', |
— | — | @@ -381,6 +397,12 @@ |
382 | 398 | ## models/storage |
383 | 399 | # poll |
384 | 400 | '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', |
385 | 407 | # question storage; qp_TextQuestionData is very small, thus kept in the same file; |
386 | 408 | 'model/qp_questiondata.php' => array( 'qp_QuestionData', 'qp_TextQuestionData' ), |
387 | 409 | # collection of the questions |
— | — | @@ -553,6 +575,7 @@ |
554 | 576 | global $qp_enable_showresults; // deprecated since v0.6.5 |
555 | 577 | global $qp_AnonForwardedFor; // deprecated since v0.6.5 |
556 | 578 | global $wgUser; |
| 579 | + self::$output = $output; |
557 | 580 | self::$article = $article; |
558 | 581 | self::$title = $title; |
559 | 582 | # in MW v1.15 / v1.16 user object was stub; |
— | — | @@ -566,6 +589,7 @@ |
567 | 590 | $user = $wgUser; |
568 | 591 | } |
569 | 592 | self::$user = $user; |
| 593 | + self::$request = $request; |
570 | 594 | if ( isset( $qp_AnonForwardedFor ) ) { |
571 | 595 | self::$anon_forwarded_for = $qp_AnonForwardedFor; |
572 | 596 | } |
— | — | @@ -619,8 +643,8 @@ |
620 | 644 | } |
621 | 645 | global $wgQPollFunctionsHook; |
622 | 646 | # 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' ) ); |
625 | 649 | $wgQPollFunctionsHook = new qp_FunctionsHook(); |
626 | 650 | # setup function hook |
627 | 651 | $parser->setFunctionHook( 'qpuserchoice', array( &$wgQPollFunctionsHook, 'qpuserchoice' ), SFH_OBJECT_ARGS ); |
Index: trunk/extensions/QPoll/includes/qp_functionshook.php |
— | — | @@ -91,7 +91,7 @@ |
92 | 92 | if ( preg_match( qp_Setup::PREG_POSITIVE_INT4_MATCH, $this->question_id ) ) { |
93 | 93 | $this->question_id = intval( $this->question_id ); |
94 | 94 | $this->pollStore->loadQuestions(); |
95 | | - $this->pollStore->setLastUser( qp_Setup::getCurrUserName(), false ); |
| 95 | + $this->pollStore->setLastUser( qp_Setup::getCurrUserName() ); |
96 | 96 | $this->pollStore->loadUserVote(); |
97 | 97 | $this->error_message = 'missing_question_id'; |
98 | 98 | if ( array_key_exists( $this->question_id, $this->pollStore->Questions ) ) { |
Index: trunk/extensions/QPoll/includes/qp_xlswriter.php |
— | — | @@ -64,7 +64,9 @@ |
65 | 65 | static $wb; |
66 | 66 | # an instance of XLS worksheet (only one currently is used) |
67 | 67 | 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 |
69 | 71 | static $format; |
70 | 72 | # current row number in a worksheet (pointer) |
71 | 73 | static $rownum = 0; |
— | — | @@ -92,10 +94,15 @@ |
93 | 95 | */ |
94 | 96 | function addFormats( $formats ) { |
95 | 97 | foreach ( $formats as $fkey => $fdef ) { |
| 98 | + self::$fdef[$fkey] = $fdef; |
96 | 99 | self::$format[$fkey] = self::$wb->addformat( $fdef ); |
97 | 100 | } |
98 | 101 | } |
99 | 102 | |
| 103 | + function getFormatDefinition( $fkey ) { |
| 104 | + return self::$fdef[$fkey]; |
| 105 | + } |
| 106 | + |
100 | 107 | function getFormat( $fkey ) { |
101 | 108 | return self::$format[$fkey]; |
102 | 109 | } |
Index: trunk/extensions/QPoll/interpretation/qp_interpret.php |
— | — | @@ -109,8 +109,10 @@ |
110 | 110 | if ( !is_array( $result ) ) { |
111 | 111 | return $interpResult->setError( wfMsg( 'qp_error_interpretation_no_return' ) ); |
112 | 112 | } |
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']; |
115 | 117 | } |
116 | 118 | if ( isset( $result['error'] ) && is_array( $result['error'] ) ) { |
117 | 119 | # initialize $interpResult->qpcErrors[] member array |
Index: trunk/extensions/QPoll/view/proposal/qp_tabularquestionproposalview.php |
— | — | @@ -164,7 +164,7 @@ |
165 | 165 | # end of new span |
166 | 166 | if ( $this->ctrl->poll->mBeingCorrected && |
167 | 167 | !$spanState->wasChecked && |
168 | | - $this->ctrl->mRequest->getVal( $name ) != $value ) { |
| 168 | + qp_Setup::$request->getVal( $name ) != $value ) { |
169 | 169 | # the span (a part of proposal) was submitted but unanswered |
170 | 170 | $this->prependErrorMessage( wfMsg( 'qp_error_unanswered_span' ), 'NA' ); |
171 | 171 | # highlight current span to indicate an error |
Index: trunk/extensions/QPoll/view/poll/qp_pollview.php |
— | — | @@ -91,7 +91,6 @@ |
92 | 92 | * @return rendered "final" html |
93 | 93 | */ |
94 | 94 | function renderPoll() { |
95 | | - global $wgOut, $wgRequest; |
96 | 95 | $pollStore = $this->ctrl->pollStore; |
97 | 96 | # Generates the output. |
98 | 97 | $qpoll_div = array( '__tag' => 'div', 'class' => 'qpoll' ); |
— | — | @@ -140,7 +139,8 @@ |
141 | 140 | $submitBtn[ 'disabled' ] = 'disabled'; |
142 | 141 | } |
143 | 142 | # 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() ) { |
145 | 145 | $submitBtn[ 'disabled' ] = 'disabled'; |
146 | 146 | } |
147 | 147 | $submitBtn[ 'value' ] = wfMsgHtml( $submitMsg ); |
Index: trunk/extensions/QPoll/view/xls/qp_xlspoll.php |
— | — | @@ -145,7 +145,7 @@ |
146 | 146 | if ( isset( $line['keys'] ) ) { |
147 | 147 | # current node is associative array |
148 | 148 | $this->writeRowLn( 0, $line['keys'], 'odd' ); |
149 | | - $ws->writeRowLn( 0, $line['vals'] ); |
| 149 | + $this->writeRowLn( 0, $line['vals'] ); |
150 | 150 | } else { |
151 | 151 | $this->writeLn( 0, $line['vals'] ); |
152 | 152 | } |
Index: trunk/extensions/QPoll/view/xls/qp_xlstextquestion.php |
— | — | @@ -4,9 +4,10 @@ |
5 | 5 | |
6 | 6 | function __construct( $xls_fname = null ) { |
7 | 7 | parent::__construct( $xls_fname ); |
| 8 | + # answered categories will be displayed with already added format 'answer' |
8 | 9 | $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 ), |
11 | 12 | ) ); |
12 | 13 | } |
13 | 14 | |
— | — | @@ -46,7 +47,7 @@ |
47 | 48 | if ( !array_key_exists( $rowNum, $voicesTable ) ) { |
48 | 49 | $voicesTable[$rowNum] = array(); |
49 | 50 | } |
50 | | - $voicesTable[$rowNum++][$rowCol] = array( $option, 'format' => 'cat_part' ); |
| 51 | + $voicesTable[$rowNum++][$rowCol] = array( $option, 'format' => 'answer' ); |
51 | 52 | } |
52 | 53 | $rowCol++; |
53 | 54 | if ( ( $rowNum - $saveRowNum ) > $rowHeight ) { |
— | — | @@ -54,15 +55,15 @@ |
55 | 56 | } |
56 | 57 | $rowNum = $saveRowNum; |
57 | 58 | } else { |
58 | | - $voicesTable[$rowNum][$rowCol++] = array( array_pop( $selected_options ), 'format' => 'cat_part' ); |
| 59 | + $voicesTable[$rowNum][$rowCol++] = array( array_pop( $selected_options ), 'format' => 'answer' ); |
59 | 60 | } |
60 | 61 | } else { |
61 | 62 | # 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' ); |
63 | 64 | } |
64 | 65 | } else { |
65 | 66 | # non-selected category (it has no selected option) |
66 | | - $voicesTable[$rowNum][$rowCol++] = array( '', 'format' => 'cat_part' ); |
| 67 | + $voicesTable[$rowNum][$rowCol++] = array( '', 'format' => 'cat_noanswer' ); |
67 | 68 | } |
68 | 69 | $catId++; |
69 | 70 | } else { |