Index: trunk/extensions/ArticleFeedback/SpecialArticleFeedback.php |
— | — | @@ -113,12 +113,19 @@ |
114 | 114 | */ |
115 | 115 | protected function renderDailyHighsAndLows( $pages, $caption ) { |
116 | 116 | global $wgOut, $wgUser; |
| 117 | + |
| 118 | + // Pre-fill page ID cache |
| 119 | + $ids = array(); |
| 120 | + foreach ( $pages as $page ) { |
| 121 | + $ids[] = $page['page']; |
| 122 | + } |
| 123 | + self::populateTitleCache( $ids ); |
117 | 124 | |
118 | 125 | $rows = array(); |
119 | 126 | if ( $pages ) { |
120 | 127 | foreach ( $pages as $page ) { |
121 | 128 | $row = array(); |
122 | | - $pageTitle = Title::newFromId( $page['page'] ); |
| 129 | + $pageTitle = self::getTitleFromID( $page['page'] ); |
123 | 130 | $row['page'] = $wgUser->getSkin()->link( $pageTitle, $pageTitle->getPrefixedText() ); |
124 | 131 | foreach ( $page['ratings'] as $id => $value ) { |
125 | 132 | $row[] = array( |
— | — | @@ -194,32 +201,45 @@ |
195 | 202 | protected function renderProblems() { |
196 | 203 | global $wgOut, $wgUser, $wgArticleFeedbackRatings; |
197 | 204 | |
| 205 | + |
| 206 | + $problems = $this->getProblems(); |
| 207 | + |
| 208 | + // Pre-fill page ID cache |
| 209 | + $ids = array(); |
| 210 | + foreach ( $problems as $page ) { |
| 211 | + $ids[] = $page['page']; |
| 212 | + } |
| 213 | + self::populateTitleCache( $ids ); |
| 214 | + |
198 | 215 | $rows = array(); |
199 | | - foreach ( $this->getProblems() as $page ) { |
| 216 | + foreach ( $problems as $page ) { |
200 | 217 | $row = array(); |
201 | | - $pageTitle = Title::newFromText( $page['page'] ); |
| 218 | + $pageTitle = self::getTitleFromID( $page['page'] ); |
202 | 219 | $row['page'] = $wgUser->getSkin()->link( $pageTitle, $pageTitle->getPrefixedText() ); |
203 | | - foreach ( $wgArticleFeedbackRatings as $category ) { |
| 220 | + foreach ( $page['ratings'] as $id => $value ) { |
204 | 221 | $row[] = array( |
205 | | - 'attr' => in_array( $category, $page['categories'] ) |
206 | | - ? array( |
207 | | - 'class' => 'articleFeedback-table-column-bad', |
208 | | - 'data-sort-value' => 0 |
209 | | - ) |
210 | | - : array( |
211 | | - 'class' => 'articleFeedback-table-column-good', |
212 | | - 'data-sort-value' => 1 |
213 | | - ), |
214 | | - 'html' => ' ' |
| 222 | + 'text' => $this->formatNumber( $value ), |
| 223 | + 'attr' => array( |
| 224 | + 'class' => 'articleFeedback-table-column-rating ' . |
| 225 | + 'articleFeedback-table-column-score-' . round( $value ) |
| 226 | + ) |
215 | 227 | ); |
216 | 228 | } |
| 229 | + $row[] = array( |
| 230 | + 'text' => $this->formatNumber( $page['average'] ), |
| 231 | + 'attr' => array( |
| 232 | + 'class' => 'articleFeedback-table-column-average ' . |
| 233 | + 'articleFeedback-table-column-score-' . round( $page['average'] ) |
| 234 | + ) |
| 235 | + ); |
217 | 236 | $rows[] = $row; |
218 | 237 | } |
219 | 238 | $this->renderTable( |
220 | 239 | wfMsg( 'articleFeedback-table-caption-recentlows' ), |
221 | 240 | array_merge( |
222 | 241 | array( wfMsg( 'articleFeedback-table-heading-page' ) ), |
223 | | - self::getCategories() |
| 242 | + self::getCategories(), |
| 243 | + array( wfMsg( 'articleFeedback-table-heading-average' ) ) |
224 | 244 | ), |
225 | 245 | $rows, |
226 | 246 | 'articleFeedback-table-recentlows' |
— | — | @@ -235,14 +255,15 @@ |
236 | 256 | $key = wfMemcKey( 'article_feedback_stats_problems' ); |
237 | 257 | $cache = $wgMemc->get( $key ); |
238 | 258 | if ( is_array( $cache )) { |
239 | | - $highs_lows = $cache; |
| 259 | + $problems = $cache; |
240 | 260 | } else { |
241 | 261 | $dbr = wfGetDB( DB_SLAVE ); |
| 262 | + $typeID = self::getStatsTypeId( 'problems' ); |
242 | 263 | // first find the freshest timestamp |
243 | 264 | $row = $dbr->selectRow( |
244 | 265 | 'article_feedback_stats', |
245 | 266 | array( 'afs_ts' ), |
246 | | - "", |
| 267 | + array( 'afs_stats_type_id' => $typeID ), |
247 | 268 | __METHOD__, |
248 | 269 | array( "ORDER BY" => "afs_ts DESC", "LIMIT" => 1 ) |
249 | 270 | ); |
— | — | @@ -262,7 +283,7 @@ |
263 | 284 | ), |
264 | 285 | array( |
265 | 286 | 'afs_ts' => $row->afs_ts, |
266 | | - 'afs_stats_type_id' => self::getStatsTypeId( 'problems' ) |
| 287 | + 'afs_stats_type_id' => $typeID |
267 | 288 | ), |
268 | 289 | __METHOD__, |
269 | 290 | array( "ORDER BY" => "afs_orderable_data" ) |
— | — | @@ -292,11 +313,12 @@ |
293 | 314 | $highs_lows = $cache; |
294 | 315 | } else { |
295 | 316 | $dbr = wfGetDB( DB_SLAVE ); |
| 317 | + $typeID = self::getStatsTypeId( 'highs_and_lows' ); |
296 | 318 | // first find the freshest timestamp |
297 | 319 | $row = $dbr->selectRow( |
298 | 320 | 'article_feedback_stats', |
299 | 321 | array( 'afs_ts' ), |
300 | | - "", |
| 322 | + array( 'afs_stats_type_id' => $typeID ), |
301 | 323 | __METHOD__, |
302 | 324 | array( "ORDER BY" => "afs_ts DESC", "LIMIT" => 1 ) |
303 | 325 | ); |
— | — | @@ -316,7 +338,7 @@ |
317 | 339 | ), |
318 | 340 | array( |
319 | 341 | 'afs_ts' => $row->afs_ts, |
320 | | - 'afs_stats_type_id' => self::getStatsTypeId( 'highs_and_lows' ) |
| 342 | + 'afs_stats_type_id' => $typeID, |
321 | 343 | ), |
322 | 344 | __METHOD__, |
323 | 345 | array( "ORDER BY" => "afs_orderable_data" ) |
— | — | @@ -374,16 +396,19 @@ |
375 | 397 | |
376 | 398 | /** |
377 | 399 | * Build data store of highs/lows for use when rendering table |
378 | | - * @param object Database result |
| 400 | + * @param object Database result or array of rows |
379 | 401 | * @return array |
380 | 402 | */ |
381 | 403 | public static function buildHighsAndLows( $result ) { |
382 | 404 | $highs_lows = array(); |
383 | 405 | foreach ( $result as $row ) { |
| 406 | + if ( is_array( $row ) ) { |
| 407 | + $row = (object)$row; |
| 408 | + } |
384 | 409 | $highs_lows[] = array( |
385 | 410 | 'page' => $row->afs_page_id, |
386 | 411 | 'ratings' => FormatJson::decode( $row->afs_data ), |
387 | | - 'average' => $row->afs_orderable_data |
| 412 | + 'average' => $row->afs_orderable_data |
388 | 413 | ); |
389 | 414 | } |
390 | 415 | return $highs_lows; |
— | — | @@ -391,16 +416,19 @@ |
392 | 417 | |
393 | 418 | /** |
394 | 419 | * Build data store of problems for use when rendering table |
395 | | - * @param object Database result |
| 420 | + * @param object Database result or array of rows |
396 | 421 | * @return array |
397 | 422 | */ |
398 | 423 | public static function buildProblems( $result ) { |
399 | 424 | $problems = array(); |
400 | 425 | foreach( $result as $row ) { |
| 426 | + if ( is_array( $row ) ) { |
| 427 | + $row = (object)$row; |
| 428 | + } |
401 | 429 | $problems[] = array( |
402 | 430 | 'page' => $row->afs_page_id, |
403 | 431 | 'ratings' => FormatJson::decode( $row->afs_data ), |
404 | | - 'average' => $row->afs_orderable_data |
| 432 | + 'average' => $row->afs_orderable_data |
405 | 433 | ); |
406 | 434 | } |
407 | 435 | return $problems; |
— | — | @@ -463,54 +491,20 @@ |
464 | 492 | ); |
465 | 493 | } |
466 | 494 | |
467 | | - /** |
468 | | - * Gets a list of articles which have recently recieved exceptionally low ratings. |
469 | | - * |
470 | | - * - Based on any rating category |
471 | | - * - Gets up to 100 most recently poorly rated articles |
472 | | - * - Only consider articles which were rated lower than 3 for 7 out of the last 10 ratings |
473 | | - * |
474 | | - * This data should be updated whenever article ratings are changed, ideally through a hook |
475 | | - */ |
476 | | - protected function getRecentLows() { |
477 | | - return array( |
478 | | - array( |
479 | | - 'page' => 'Main Page', |
480 | | - // List of rating IDs that qualify as recent lows |
481 | | - 'categories' => array( 1, 4 ), |
482 | | - ), |
483 | | - array( |
484 | | - 'page' => 'Test Article 1', |
485 | | - 'categories' => array( 1, 3 ), |
486 | | - ), |
487 | | - array( |
488 | | - 'page' => 'Test Article 2', |
489 | | - 'categories' => array( 2, 3 ), |
490 | | - ), |
491 | | - array( |
492 | | - 'page' => 'Test Article 3', |
493 | | - 'categories' => array( 3, 4 ), |
494 | | - ), |
495 | | - array( |
496 | | - 'page' => 'Test Article 4', |
497 | | - 'categories' => array( 1, 2 ), |
498 | | - ) |
499 | | - ); |
500 | | - } |
501 | | - |
502 | 495 | /* Protected Static Members */ |
503 | 496 | |
504 | 497 | protected static $categories; |
| 498 | + protected static $titleCache = array(); |
505 | 499 | |
506 | 500 | /* Protected Static Methods */ |
507 | 501 | |
508 | | - protected function formatNumber( $number ) { |
| 502 | + protected static function formatNumber( $number ) { |
509 | 503 | global $wgLang; |
510 | 504 | |
511 | 505 | return $wgLang->formatNum( number_format( $number, 2 ) ); |
512 | 506 | } |
513 | 507 | |
514 | | - protected function getCategories() { |
| 508 | + protected static function getCategories() { |
515 | 509 | global $wgArticleFeedbackRatings; |
516 | 510 | |
517 | 511 | if ( !isset( self::$categories ) ) { |
— | — | @@ -518,7 +512,8 @@ |
519 | 513 | $res = $dbr->select( |
520 | 514 | 'article_feedback_ratings', |
521 | 515 | array( 'aar_id', 'aar_rating' ), |
522 | | - array( 'aar_id' => $wgArticleFeedbackRatings ) |
| 516 | + array( 'aar_id' => $wgArticleFeedbackRatings ), |
| 517 | + __METHOD__ |
523 | 518 | ); |
524 | 519 | self::$categories = array(); |
525 | 520 | foreach ( $res as $row ) { |
— | — | @@ -527,4 +522,20 @@ |
528 | 523 | } |
529 | 524 | return self::$categories; |
530 | 525 | } |
| 526 | + |
| 527 | + protected static function getTitleFromID( $id ) { |
| 528 | + // There's no caching in Title::newFromId() so we hack our own around it |
| 529 | + if ( !isset( self::$titleCache[$id] ) ) { |
| 530 | + self::$titleCache[$id] = Title::newFromId( $id ); |
| 531 | + } |
| 532 | + return self::$titleCache[$id]; |
| 533 | + } |
| 534 | + |
| 535 | + protected static function populateTitleCache( $ids ) { |
| 536 | + $toQuery = array_diff( $ids, array_keys( self::$titleCache ) ); |
| 537 | + $titles = Title::newFromIds( $toQuery ); |
| 538 | + foreach ( $titles as $title ) { |
| 539 | + self::$titleCache[$title->getArticleID()] = $title; |
| 540 | + } |
| 541 | + } |
531 | 542 | } |
Index: trunk/extensions/ArticleFeedback/sql/AddArticleFeedbackStatsTable.sql |
— | — | @@ -9,5 +9,5 @@ |
10 | 10 | -- timestamp of insertion job |
11 | 11 | afs_ts binary(14) NOT NULL |
12 | 12 | ) /*$wgDBTableOptions*/; |
13 | | -CREATE UNIQUE INDEX /*i*/ afs_page_ts_type ON /*_*/article_feedback_stats( afs_page_id, afs_ts, afs_stats_type_id ); |
14 | | -CREATE INDEX /*i*/ afs_ts_avg_overall ON /*_*/article_feedback_stats (afs_ts, afs_orderable_data); |
| 13 | +CREATE UNIQUE INDEX /*i*/afs_type_ts_page ON /*_*/article_feedback_stats(afs_stats_type_id, afs_ts, afs_page_id); |
| 14 | +CREATE INDEX /*i*/ afs_type_ts_orderable ON /*_*/article_feedback_stats (afs_stats_type_id, afs_ts, afs_orderable_data); |
Index: trunk/extensions/ArticleFeedback/populateAFStatistics.php |
— | — | @@ -176,12 +176,12 @@ |
177 | 177 | } |
178 | 178 | |
179 | 179 | if ( $page->isProblematic() ) { |
180 | | - array_push( $problems, $page->page_id ); |
| 180 | + $problems[] = $page->page_id; |
181 | 181 | } |
182 | 182 | } |
183 | 183 | |
184 | 184 | // populate stats table with problem articles & associated data |
185 | | - // fetch stats type id - add stat type if it's non-existant |
| 185 | + // fetch stats type id - add stat type if it's non-existent |
186 | 186 | $stats_type_id = SpecialArticleFeedback::getStatsTypeId( 'problems' ); |
187 | 187 | if ( !$stats_type_id ) { |
188 | 188 | $stats_type_id = $this->addStatType( 'problems' ); |
— | — | @@ -201,28 +201,31 @@ |
202 | 202 | } |
203 | 203 | $this->output( "Done.\n" ); |
204 | 204 | |
| 205 | + // Insert the problem rows into the database |
| 206 | + $this->output( "Writing data to article_feedback_stats ...\n" ); |
| 207 | + $rowsInserted = 0; |
| 208 | + // $rows is gonna be modified by array_splice(), so make a copy for later use |
| 209 | + $rowsCopy = $rows; |
| 210 | + while( $rows ) { |
| 211 | + $batch = array_splice( $rows, 0, $this->insert_batch_size ); |
| 212 | + $this->dbw->insert( |
| 213 | + 'article_feedback_stats', |
| 214 | + $batch, |
| 215 | + __METHOD__ |
| 216 | + ); |
| 217 | + $rowsInserted += count( $batch ); |
| 218 | + $this->syncDBs(); |
| 219 | + $this->output( "Inserted " . $rowsInserted . " rows\n" ); |
| 220 | + } |
| 221 | + $this->output( "Done.\n" ); |
| 222 | + |
205 | 223 | // populate cache with current problem articles |
206 | | - // loading data into cache |
207 | 224 | $this->output( "Caching latest problems (if cache present).\n" ); |
208 | | - $key = wfMemcKey( 'article_feedback_stats_problems' ); |
209 | | - $result = $this->dbr->select( |
210 | | - 'article_feedback_stats', |
211 | | - array( |
212 | | - 'afs_page_id', |
213 | | - 'afs_orderable_data', |
214 | | - 'afs_data' |
215 | | - ), |
216 | | - array( |
217 | | - 'afs_ts' => $cur_ts, |
218 | | - 'afs_stats_type_id' => $stats_type_id |
219 | | - ), |
220 | | - __METHOD__, |
221 | | - array( "ORDER BY" => "afs_orderable_data" ) |
222 | | - ); |
223 | 225 | // grab the article feedback special page so we can reuse the data structure building code |
224 | 226 | // FIXME this logic should not be in the special page class |
225 | | - $problems = SpecialArticleFeedback::buildProblems( $result ); |
| 227 | + $problems = SpecialArticleFeedback::buildProblems( $rowsCopy ); |
226 | 228 | // stash the data structure in the cache |
| 229 | + $key = wfMemcKey( 'article_feedback_stats_problems' ); |
227 | 230 | $wgMemc->set( $key, $problems, 86400 ); |
228 | 231 | $this->output( "Done.\n" ); |
229 | 232 | } |
— | — | @@ -300,6 +303,8 @@ |
301 | 304 | // insert data to db |
302 | 305 | $this->output( "Writing data to article_feedback_stats ...\n" ); |
303 | 306 | $rowsInserted = 0; |
| 307 | + // $rows is gonna be modified by array_splice(), so make a copy for later use |
| 308 | + $rowsCopy = $rows; |
304 | 309 | while( $rows ) { |
305 | 310 | $batch = array_splice( $rows, 0, $this->insert_batch_size ); |
306 | 311 | $this->dbw->insert( |
— | — | @@ -316,26 +321,12 @@ |
317 | 322 | // loading data into cache |
318 | 323 | $this->output( "Caching latest highs/lows (if cache present).\n" ); |
319 | 324 | $key = wfMemcKey( 'article_feedback_stats_highs_lows' ); |
320 | | - $result = $this->dbr->select( |
321 | | - 'article_feedback_stats', |
322 | | - array( |
323 | | - 'afs_page_id', |
324 | | - 'afs_orderable_data', |
325 | | - 'afs_data' |
326 | | - ), |
327 | | - array( |
328 | | - 'afs_ts' => $cur_ts, |
329 | | - 'afs_stats_type_id' => $stats_type_id |
330 | | - ), |
331 | | - __METHOD__, |
332 | | - array( "ORDER BY" => "afs_orderable_data" ) |
333 | | - ); |
334 | 325 | // grab the article feedback special page so we can reuse the data structure building code |
335 | 326 | // FIXME this logic should not be in the special page class |
336 | | - $highs_lows = SpecialArticleFeedback::buildHighsAndLows( $result ); |
| 327 | + $highs_lows = SpecialArticleFeedback::buildHighsAndLows( $rowsCopy ); |
337 | 328 | // stash the data structure in the cache |
338 | 329 | $wgMemc->set( $key, $highs_lows, 86400 ); |
339 | | - $this->output( "Done\n" ); |
| 330 | + $this->output( "Done\n" ); |
340 | 331 | } |
341 | 332 | |
342 | 333 | /** |
— | — | @@ -364,7 +355,7 @@ |
365 | 356 | 'aa_page_id', |
366 | 357 | 'aa_rating_value', |
367 | 358 | ), |
368 | | - array( 'aa_timestamp >= ' . $this->dbr->addQuotes( $ts )), |
| 359 | + array( 'aa_timestamp >= ' . $this->dbr->addQuotes( $this->dbr->timestamp( $ts ) ) ), |
369 | 360 | __METHOD__, |
370 | 361 | array() |
371 | 362 | ); |
— | — | @@ -390,6 +381,7 @@ |
391 | 382 | // fetch the page from the page store referentially so we can |
392 | 383 | // perform actions on it that will automagically be saved in the |
393 | 384 | // object for easy access later |
| 385 | + |
394 | 386 | $page =& $pages->getPage( $row->aa_page_id ); |
395 | 387 | |
396 | 388 | // determine the unique hash for a given rating set (page rev + user identifying info) |
— | — | @@ -584,7 +576,7 @@ |
585 | 577 | */ |
586 | 578 | public $pages = array(); |
587 | 579 | |
588 | | - public function getPage( $page_id ) { |
| 580 | + public function &getPage( $page_id ) { |
589 | 581 | if ( !isset( $this->pages[ $page_id ] )) { |
590 | 582 | $this->addPage( $page_id ); |
591 | 583 | } |