Mattflaschen has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/97691


Change subject: New API for selecting a task; related refactoring and minor fm
......................................................................

New API for selecting a task; related refactoring and minor fm

* New API module GettingStartedGetPages for getting one or more pages for a 
given task
* Used by special page
* New ArticleFilter for secondary criterion (other than Redis set) that can 
allow or veto an article
** Broken out from the special page.
* Some renaming for clarity
* Small README fix about srand.

Bug: 55598
Bug: 55773
Change-Id: Icaf84e75d95bb06d108137221aa13e99003355e1
---
D CategoryRoulette.php
M GettingStarted.php
A PageFilter.php
M README
M SpecialGettingStarted.php
A api/ApiGettingStartedGetPages.php
6 files changed, 230 insertions(+), 148 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/GettingStarted 
refs/changes/91/97691/1

diff --git a/CategoryRoulette.php b/CategoryRoulette.php
deleted file mode 100644
index e681c2a..0000000
--- a/CategoryRoulette.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-
-namespace GettingStarted;
-
-use Title;
-
-/**
- * Helper class for retrieving random pages from a category.
- * See Redis requirements in README.
- *
- * Sample usage:
- * <code>
- * $category = Category::newFromName( 'All_articles_needing_copy_edit' );
- * $roulette = new CategoryRoulette( $category );
- * $pages = $roulette->getRandomArticles( 3 );
- * </code>
- *
- */
-class CategoryRoulette {
-
-       const MAX_ATTEMPTS = 100;
-
-       /** @var Category **/
-       public $category = null;
-
-       /**
-        * Constructor.
-        * @param $category Category.
-        */
-       public function __construct( $category ) {
-               $this->category = $category;
-       }
-
-       /**
-        * Get a random set of $numWanted unique pages in the
-        * category. If fewer than $numWanted pages exist in category,
-        * return as many as are available. It is up to the caller to decide
-        * how to handle the deficit.
-        *
-        * @param $numWanted int Number of unique pages to get.
-        * @return array Set of $numWanted unique pages (or however many
-        *   were available, if the desired count was not satisfiable).
-        */
-       public function getRandomArticles( $numWanted ) {
-               $key = RedisCategorySync::makeCategoryKey( $this->category );
-
-               $redis = RedisCategorySync::getClient();
-               if ( !$redis ) {
-                       wfDebugLog( 'GettingStarted', "Unable to acquire redis 
connection.\n" );
-                       return array();
-               }
-
-               $articleIDs = array();
-               $attempts = 0;
-               while ( count( $articleIDs ) < $numWanted ) {
-                       $attempts++;
-                       // Sanity check to prevent calling srand too many times
-                       if ( $attempts >= self::MAX_ATTEMPTS ) {
-                               wfDebugLog( 'GettingStarted', 'Returning early 
after ' . self::MAX_ATTEMPTS . ".\n" );
-                               return Title::newFromIDs( $articleIDs );
-                       }
-                       try {
-                               $randomID = $redis->sRandMember( $key );
-                               if ( !in_array( $randomID, $articleIDs, true ) 
) {
-                                       $articleIDs[] = $randomID;
-                               }
-                       } catch ( RedisException $e ) {
-                               wfDebugLog( 'GettingStarted', 'Redis exception: 
' . $e->getMessage() . ".  Returning early.\n" );
-                               return Title::newFromIDs( $articleIDs );
-                       }
-               }
-
-               return Title::newFromIDs( $articleIDs );
-       }
-}
diff --git a/GettingStarted.php b/GettingStarted.php
index 2f6718d..7b60561 100644
--- a/GettingStarted.php
+++ b/GettingStarted.php
@@ -91,17 +91,22 @@
 
 $wgAutoloadClasses += array(
        'GettingStarted\SpecialGettingStarted' => __DIR__ . 
'/SpecialGettingStarted.php',
-       'GettingStarted\Hooks'   => __DIR__ . '/Hooks.php',
-       'GettingStarted\RedisCategorySync'     => __DIR__ . 
'/RedisCategorySync.php',
-       'GettingStarted\CategoryRoulette'      => __DIR__ . 
'/CategoryRoulette.php',
+       'GettingStarted\Hooks' => __DIR__ . '/Hooks.php',
+       'GettingStarted\RedisCategorySync' => __DIR__ . 
'/RedisCategorySync.php',
+       'GettingStarted\PageFilter' => __DIR__ . '/PageFilter.php',
+       'GettingStarted\ApiGettingStartedGetPages' => __DIR__ . 
'/api/ApiGettingStartedGetPages.php',
 );
 
 $wgExtensionMessagesFiles[ 'GettingStarted' ] = __DIR__ . 
'/GettingStarted.i18n.php';
 $wgExtensionMessagesFiles[ 'GettingStartedAlias' ] = __DIR__ . 
'/GettingStarted.alias.php';
 
+// Special pages
 $wgSpecialPages[ 'GettingStarted' ] = 'GettingStarted\SpecialGettingStarted';
 $wgSpecialPageGroups[ 'GettingStarted' ] = 'users';
 
+// APIs
+$wgAPIModules['gettingstartedgetpages'] = 
'GettingStarted\ApiGettingStartedGetPages';
+
 // Modules
 
 $gettingStartedModuleInfo = array(
diff --git a/PageFilter.php b/PageFilter.php
new file mode 100644
index 0000000..eaa3520
--- /dev/null
+++ b/PageFilter.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace GettingStarted;
+
+use Title, User;
+
+/**
+  Approve or reject a given page for suitability with GettingStarted.
+*/
+class PageFilter {
+       const MAX_PAGE_LENGTH = 10000;
+
+       /** @var User */
+       protected $user;
+
+       /** @var Title */
+       protected $excludedTitle;
+
+       /**
+        * Constructor.
+        *
+        * @param User $user user object, for permissions checks
+        * @param Title $excludedTitle optional title to exclude, to avoid 
consecutive duplicates
+        */
+       public function __construct( User $user, Title $excludedTitle = null ) {
+               $this->user = $user;
+               $this->excludedTitle = $excludedTitle;
+       }
+
+       protected function inExcludedCategories( Title $title ) {
+               global $wgGettingStartedExcludedCategories;
+
+               $articleID = $title->getArticleID();
+
+               $dbr = wfGetDB( DB_SLAVE );
+               foreach( $wgGettingStartedExcludedCategories as $cat ) {
+                       $res = $dbr->selectRow( 'categorylinks', '1', array(
+                               'cl_from' => $articleID,
+                               'cl_to' => $cat,
+                       ), __METHOD__ );
+
+                       if ( $res !== false ) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
+       public function isAllowedPage( Title $title ) {
+               $length = $title->getLength();
+               $passesExclude = $this->excludedTitle === null ||
+                       !$title->equals( $this->excludedTitle );
+               return $length > 0
+                       && $length <= self::MAX_PAGE_LENGTH
+                       // RedisCategorySync ignores category changes outside 
NS_MAIN,
+                       // but the API can still access pages outside.
+                       && $title->inNamespace( NS_MAIN )
+                       && $passesExclude
+                       && $title->userCan( 'edit', $this->user )
+                       && !$this->inExcludedCategories( $title );
+
+
+       }
+}
+
diff --git a/README b/README
index 87a5127..b9c31d2 100644
--- a/README
+++ b/README
@@ -34,10 +34,6 @@
 * phpredis extension installed on the same machine, and Redis installed on some
 machine that can be accessed.
 
-Since CategoryRoulette uses the two-argument version of
-SRANDMEMBER, it requires Redis 2.6+ for both the server and
-the PHP extension.
-
 That means phpredis needs to be (re-)compiled against 2.6 if
 upgrading from 2.4.
 
diff --git a/SpecialGettingStarted.php b/SpecialGettingStarted.php
index 02a0f8b..9f11c23 100644
--- a/SpecialGettingStarted.php
+++ b/SpecialGettingStarted.php
@@ -5,34 +5,8 @@
 use Html, Linker, SpecialPage, Title;
 
 class SpecialGettingStarted extends SpecialPage {
-
-       const MAX_ARTICLE_LENGTH = 10000;
-       const MAX_ATTEMPTS = 20;  // Somewhat arbitrary for now.
-
        public function __construct() {
                parent::__construct( 'GettingStarted' );
-       }
-
-       /**
-        * Determines if title is an allowed task, based on user and 
configuration.
-        *
-        * @param Title $title title of article
-        * @param User $user user to check
-        * @param string|null $excludedPageName page name to be excluded, in in 
getPrefixedDBkey() format,
-        *  or null for no exclusion
-        */
-       protected function isAllowedArticle( Title $title, \User $user, 
$excludedPageName ) {
-               $length = $title->getLength();
-               $passesExclude = $excludedPageName === null ||
-                       $title->getPrefixedDBkey() !== $excludedPageName;
-               return $length > 0
-                       && $length <= self::MAX_ARTICLE_LENGTH
-                       // RedisCategorySync ignores category changes outside 
NS_MAIN,
-                       // but CategoryRoulette can still return pages outside.
-                       && $title->inNamespace( NS_MAIN )
-                       && $passesExclude
-                       && $title->userCan( 'edit', $user )
-                       && !$this->inExcludedCategories( $title );
        }
 
        /**
@@ -61,31 +35,36 @@
         * constraints.
         *
         * @param string $taskName task name
-        * @return bool whether they were redirected
+        * @return Title|false page to send user to, or false on failure
         */
        public function chooseTitleForTask( $taskName ) {
-               global $wgGettingStartedTasks;
+               $request = new \DerivativeRequest(
+                       $this->getRequest(),
+                       array(
+                               'action' => 'gettingstartedgetpages',
+                               'taskname' => $taskName,
+                               'excludedTitle' => $this->getRequest()->getVal( 
'exclude' ),
+                               'count' => 1,
+                       ),
+                       false // GET request
+               );
 
-               if ( !isset( $wgGettingStartedTasks[$taskName] ) ) {
+               $api = new \ApiMain(
+                       $request,
+                       false // no writes
+               );
+
+               try {
+                       $api->execute();
+                       $result = $api->getResult()->getData();
+                       if ( isset( 
$result['gettingstartedgetpages']['titles'][0] ) ) {
+                               return Title::newFromText( 
$result['gettingstartedgetpages']['titles'][0] );
+                       } else {
+                               return false;
+                       }
+               } catch ( \UsageException $ex ) {
                        return false;
                }
-               $task = $wgGettingStartedTasks[$taskName];
-               $taskCategory = \Category::newFromName( $task['category'] );
-               $roulette = new CategoryRoulette( $taskCategory );
-               $user = $this->getUser();
-
-               $excludedPageName = $this->getRequest()->getVal( 'exclude' );
-
-               for ( $i = 0; $i < self::MAX_ATTEMPTS; $i++ ) {
-                       $titles = $roulette->getRandomArticles( 1 );
-                       if ( count( $titles ) === 1
-                               && $this->isAllowedArticle( $titles[0], $user, 
$excludedPageName )
-                       ) {
-                               return $titles[0];
-                       }
-               }
-
-               return false;
        }
 
        /**
@@ -175,26 +154,6 @@
                        'ext.gettingstarted',
                        'ext.gettingstarted.specialPage',
                ) );
-       }
-
-       public function inExcludedCategories( Title $title ) {
-               global $wgGettingStartedExcludedCategories;
-
-               $articleID = $title->getArticleID();
-
-               $dbr = wfGetDB( DB_SLAVE );
-               foreach( $wgGettingStartedExcludedCategories as $cat ) {
-                       $res = $dbr->selectRow( 'categorylinks', '1', array(
-                               'cl_from' => $articleID,
-                               'cl_to' => $cat,
-                       ), __METHOD__ );
-
-                       if ( $res !== false ) {
-                               return true;
-                       }
-               }
-
-               return false;
        }
 
        /**
diff --git a/api/ApiGettingStartedGetPages.php 
b/api/ApiGettingStartedGetPages.php
new file mode 100644
index 0000000..fe6b87f
--- /dev/null
+++ b/api/ApiGettingStartedGetPages.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace GettingStarted;
+
+use ApiBase, Category, Title;
+
+class ApiGettingStartedGetPages extends ApiBase {
+       const MAX_ATTEMPTS = 100;
+
+       public function __construct( $query, $moduleName ) {
+               parent::__construct( $query, $moduleName );
+       }
+
+       public function execute() {
+               global $wgGettingStartedTasks;
+
+               $result = $this->getResult();
+
+               // For PageFilter and specifically userCan( 'edit' )
+               $user = $this->getUser();
+
+               $taskName = $this->getParameter( 'taskname' );
+               $excludedTitle = Title::newFromText( $this->getParameter( 
'excludedTitle' ) );
+               $count = $this->getParameter( 'count' );
+
+               if ( !isset( $wgGettingStartedTasks[$taskName] ) ) {
+                       $this->dieUsage( 'Invalid value for "taskName"', 
'gettingstarted-invalidtaskname' );
+               }
+
+               $category = Category::newFromName( 
$wgGettingStartedTasks[$taskName]['category'] );
+               $pageFilter = new PageFilter( $user, $excludedTitle );
+
+               $titles = self::getRandomArticles( $count, $category, 
$pageFilter );
+               $data = array(
+                       'titles' => array()
+               );
+               foreach ( $titles as $title ) {
+                       $data['titles'][] = $title->getPrefixedText();
+               }
+               $result->setIndexedTagName( $data['titles'], 'title' );
+               $result->addValue( null, $this->getModuleName(), $data );
+       }
+
+       /**
+        * Get a random set of $numWanted unique pages in the
+        * category. If fewer than $numWanted pages exist in category,
+        * return as many as are available. It is up to the caller to decide
+        * how to handle the deficit.
+        *
+        * @param int $numWanted Number of unique pages to get.
+        * @param Category $category category to choose from
+        * @param PageFilter $pageFilter filter than can approve or reject a 
page
+        * @return array Set of $numWanted unique Title objects (or however many
+        *   were available, if the desired count was not satisfiable).
+        */
+       protected function getRandomArticles( $numWanted, Category $category, 
PageFilter $pageFilter ) {
+               $key = RedisCategorySync::makeCategoryKey( $category );
+
+               $redis = RedisCategorySync::getClient();
+               if ( !$redis ) {
+                       wfDebugLog( 'GettingStarted', "Unable to acquire redis 
connection.\n" );
+                       return array();
+               }
+
+               // Map article ID to Title.  At the end, we simply return a 
non-associative array of Titles.
+               // However, sRandMember can return the same ID more than once.  
This allows us to easily remove
+               // avoid these duplicates with array_key_exists.
+               $titles = array();
+
+               $attempts = 0;
+               while ( count( $titles ) < $numWanted ) {
+                       $attempts++;
+                       // Sanity check to prevent calling srand or filter too 
many times
+                       if ( $attempts >= self::MAX_ATTEMPTS ) {
+                               wfDebugLog( 'GettingStarted', 'Returning early 
after ' . self::MAX_ATTEMPTS . ".\n" );
+                               return array_values( $titles );
+                       }
+                       try {
+                               $randomArticleID = $redis->sRandMember( $key );
+                               if ( !array_key_exists( $randomArticleID, 
$titles ) ) {
+                                       $title = Title::newFromID( 
$randomArticleID );
+                                       if ( $pageFilter->isAllowedPage( $title 
) ) {
+                                               $titles[$randomArticleID] = 
$title;
+                                       }
+                               }
+                       } catch ( RedisException $e ) {
+                               wfDebugLog( 'GettingStarted', 'Redis exception: 
' . $e->getMessage() . ".  Returning early.\n" );
+                               return array_values( $titles );
+                       }
+               }
+
+               return array_values( $titles );
+       }
+
+       public function getDescription() {
+               return array(
+                       'This API is for getting one or more GettingStarted 
tasks'
+               );
+       }
+
+       public function getParamDescriptions() {
+               return array(
+                       'taskname' => 'Task name',
+                       'excludedTitle' => 'Full title of article',
+                       'count' => 'Requested count; will attempt to fetch this 
exact number, but may fetch fewer if no more are found after multiple attempts'
+               );
+       }
+
+       public function getAllowedParams() {
+               return array(
+                       'taskname' => array(
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_REQUIRED => true,
+                       ),
+                       'excludedTitle' => array(
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_REQUIRED => false,
+                       ),
+                       'count' => array(
+                               ApiBase::PARAM_TYPE => 'integer',
+                               ApiBase::PARAM_REQUIRED => true,
+                       ),
+               );
+       }
+
+       public function getExamples() {
+               return array(
+                       
'api.php?action=gettingstartedgetpages&taskname=copyedit&excludedTitle=Earth&count=1',
+               );
+       }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/97691
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Icaf84e75d95bb06d108137221aa13e99003355e1
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/GettingStarted
Gerrit-Branch: master
Gerrit-Owner: Mattflaschen <[email protected]>
Gerrit-Reviewer: jenkins-bot

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to