Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ListTaskCounts
0.00% covered (danger)
0.00%
0 / 85
0.00% covered (danger)
0.00%
0 / 6
462
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 getTaskTypesAndTopics
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getStats
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
30
 reportTaskCounts
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 printResults
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3namespace GrowthExperiments\Maintenance;
4
5use FormatJson;
6use GrowthExperiments\GrowthExperimentsServices;
7use GrowthExperiments\NewcomerTasks\CachedSuggestionsInfo;
8use GrowthExperiments\NewcomerTasks\ConfigurationLoader\ConfigurationLoader;
9use GrowthExperiments\NewcomerTasks\ConfigurationLoader\TopicDecorator;
10use GrowthExperiments\NewcomerTasks\SuggestionsInfo;
11use Maintenance;
12use MediaWiki\MediaWikiServices;
13
14$IP = getenv( 'MW_INSTALL_PATH' );
15if ( $IP === false ) {
16    $IP = __DIR__ . '/../../..';
17}
18require_once "$IP/maintenance/Maintenance.php";
19
20/**
21 * List the number of tasks available for each topic
22 */
23class ListTaskCounts extends Maintenance {
24
25    /** @var string 'growth' or 'ores' */
26    private $topicType;
27
28    /** @var ConfigurationLoader */
29    private $configurationLoader;
30
31    public function __construct() {
32        parent::__construct();
33        $this->requireExtension( 'GrowthExperiments' );
34
35        $this->addDescription( 'List the number of tasks available for each topic.' );
36        $this->addOption( 'tasktype', 'Task types to query, specify multiple times for multiple ' .
37                                      'task types. Defaults to all task types.', false, true, false, true );
38        $this->addOption( 'topic', 'Topics to query, specify multiple times for multiple ' .
39                                      'topics. Defaults to all topics.', false, true, false, true );
40        $this->addOption( 'topictype', "Topic type to use ('ores' or 'growth').", false, true );
41        $this->addOption( 'statsd', 'Send topic counts to statsd. For link recommendations only.' );
42        $this->addOption( 'output', "'ascii-table' (default), 'json' or 'none'", false, true );
43    }
44
45    /** @inheritDoc */
46    public function execute() {
47        if ( $this->getConfig()->get( 'GENewcomerTasksRemoteApiUrl' ) ) {
48            $this->output( "Local tasks disabled\n" );
49            return;
50        }
51
52        $this->topicType = $this->getOption( 'topictype', 'growth' );
53        if ( !in_array( $this->topicType, [ 'ores', 'growth' ], true ) ) {
54            $this->fatalError( 'topictype must be one of: growth, ores' );
55        }
56
57        $growthServices = GrowthExperimentsServices::wrap( MediaWikiServices::getInstance() );
58        $newcomerTaskConfigurationLoader = $growthServices->getNewcomerTasksConfigurationLoader();
59        $this->configurationLoader = new TopicDecorator(
60            $newcomerTaskConfigurationLoader,
61            $this->topicType == 'ores'
62        );
63
64        [ $taskTypes, $topics ] = $this->getTaskTypesAndTopics();
65        [ $taskCounts, $taskTypeCounts, $topicCounts ] = $this->getStats( $taskTypes, $topics );
66        if ( $this->hasOption( 'statsd' ) ) {
67            $this->reportTaskCounts( $taskCounts, $taskTypeCounts );
68        }
69        $this->printResults( $taskTypes, $topics, $taskCounts, $taskTypeCounts, $topicCounts );
70    }
71
72    /**
73     * Get task types and topics to list task counts for
74     *
75     * @return array{0:string[],1:string[]} [ task type ID list, topic ID list ]
76     */
77    private function getTaskTypesAndTopics(): array {
78        $allTaskTypes = array_keys( $this->configurationLoader->getTaskTypes() );
79        $taskTypes = $this->getOption( 'tasktype', $allTaskTypes );
80        if ( array_diff( $taskTypes, $allTaskTypes ) ) {
81            $this->fatalError( 'Invalid task types: ' . implode( ', ', array_diff( $taskTypes, $allTaskTypes ) ) );
82        }
83
84        $allTopics = array_keys( $this->configurationLoader->getTopics() );
85        $topics = $this->getOption( 'topic', $allTopics );
86        if ( array_diff( $topics, $allTopics ) ) {
87            $this->fatalError( 'Invalid topics: ' . implode( ', ', array_diff( $topics, $allTopics ) ) );
88        }
89
90        return [ $taskTypes, $topics ];
91    }
92
93    /**
94     * @param string[] $taskTypes List of task type IDs to count for
95     * @param string[] $topics List of topic IDs to count for
96     * @return array{0:int[][],1:int[],2:int[]} An array with three elements:
97     *   - a matrix of task type ID => topic ID => count
98     *   - a list of task type ID => total count
99     *   - a list of topic ID => total count.
100     *   Note that the second and third elements are not the same as column and row totals
101     *   of the first element, because an article can have multiple topics and multiple
102     *   task types, and not all articles have task types, and some (the recently created)
103     *   might not even have topics.
104     */
105    private function getStats( $taskTypes, $topics ): array {
106        $taskCounts = $taskTypeCounts = $topicCounts = [];
107        $mwServices = MediaWikiServices::getInstance();
108        $services = GrowthExperimentsServices::wrap( $mwServices );
109        // Cache stats for Growth topics since they're also used in SpecialNewcomerTasksInfo
110        $shouldCacheStats = $this->topicType === 'growth';
111        $suggestionsInfoService = new SuggestionsInfo(
112            $services->getTaskSuggesterFactory(),
113            $services->getTaskTypeHandlerRegistry(),
114            $this->configurationLoader
115        );
116        if ( $shouldCacheStats ) {
117            $suggestionsInfo = new CachedSuggestionsInfo(
118                $suggestionsInfoService,
119                $mwServices->getMainWANObjectCache()
120            );
121        } else {
122            $suggestionsInfo = $suggestionsInfoService;
123        }
124        $info = $suggestionsInfo->getInfo( [ 'resetCache' => true ] );
125
126        $topicsInfo = $info['topics'] ?? [];
127        $tasksInfo = $info['tasks'] ?? [];
128
129        foreach ( $taskTypes as $taskType ) {
130            foreach ( $topics as $topic ) {
131                $taskInfoForTopic = $topicsInfo[ $topic ][ 'tasks' ];
132                $taskCounts[ $taskType ][ $topic ] = $taskInfoForTopic[ $taskType ][ 'count' ];
133            }
134            $taskTypeCounts[ $taskType ] = $tasksInfo[ $taskType ][ 'totalCount' ];
135        }
136        foreach ( $topics as $topic ) {
137            $topicCounts[ $topic ] = $topicsInfo[ $topic ][ 'totalCount' ];
138        }
139        return [ $taskCounts, $taskTypeCounts, $topicCounts ];
140    }
141
142    /**
143     * @param int[][] $taskCounts task type ID => topic ID => count
144     * @param int[] $taskTypeCounts task type ID => total count
145     */
146    private function reportTaskCounts( array $taskCounts, array $taskTypeCounts ): void {
147        $dataFactory = MediaWikiServices::getInstance()->getPerDbNameStatsdDataFactory();
148        foreach ( $taskTypeCounts as $taskTypeId => $taskTypeCount ) {
149            $dataFactory->updateCount( "growthexperiments.tasktypecount.$taskTypeId", $taskTypeCount );
150        }
151    }
152
153    /**
154     * @param string[] $taskTypes List of task type IDs to count for
155     * @param string[] $topics List of topic IDs to count for
156     * @param int[][] $taskCounts task type ID => topic ID => count
157     * @param int[] $taskTypeCounts task type ID => total count
158     * @param int[] $topicCounts topic ID => total count
159     */
160    private function printResults( $taskTypes, $topics, array $taskCounts, array $taskTypeCounts, array $topicCounts ) {
161        $output = $this->getOption( 'output', 'ascii-table' );
162        if ( $output === 'none' ) {
163            return;
164        } elseif ( $output === 'json' ) {
165            $this->output( FormatJson::encode( [
166                'taskCounts' => $taskCounts,
167                'taskTypeCounts' => $taskTypeCounts,
168                'topicCounts' => $topicCounts,
169            ], false, FormatJson::UTF8_OK ) );
170            return;
171        }
172
173        // Output header
174        $this->output( str_pad( 'Topic', 25, ' ' ) . ' ' );
175        foreach ( $taskTypes as $taskType ) {
176            $this->output( str_pad( $taskType, 10, ' ' ) . ' ' );
177        }
178        $this->output( "\n" . str_repeat( '-', 80 ) . "\n" );
179
180        foreach ( $topics as $topic ) {
181            $this->output( str_pad( $topic, 25, ' ' ) . ' ' );
182            foreach ( $taskTypes as $taskType ) {
183                $this->output( str_pad( (string)$taskCounts[$taskType][$topic], 3, ' ', STR_PAD_RIGHT )
184                               . str_repeat( ' ', 8 ) );
185            }
186            $this->output( "\n" );
187        }
188    }
189
190}
191
192$maintClass = ListTaskCounts::class;
193require_once RUN_MAINTENANCE_IF_MAIN;