Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 98
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 / 92
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 / 10
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 GrowthExperiments\GrowthExperimentsServices;
6use GrowthExperiments\NewcomerTasks\CachedSuggestionsInfo;
7use GrowthExperiments\NewcomerTasks\ConfigurationLoader\ConfigurationLoader;
8use GrowthExperiments\NewcomerTasks\ConfigurationLoader\TopicDecorator;
9use GrowthExperiments\NewcomerTasks\SuggestionsInfo;
10use MediaWiki\Json\FormatJson;
11use MediaWiki\Maintenance\Maintenance;
12use MediaWiki\WikiMap\WikiMap;
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( $this->getServiceContainer() );
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 = $this->getServiceContainer();
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        $wiki = WikiMap::getCurrentWikiId();
148        $counter = $this->getServiceContainer()->getStatsFactory()
149            ->withComponent( 'GrowthExperiments' )
150            ->getCounter( 'tasktype_count' );
151        foreach ( $taskTypeCounts as $taskTypeId => $taskTypeCount ) {
152            $counter
153                ->setLabel( 'wiki', $wiki )
154                ->setLabel( 'tasktype', $taskTypeId )
155                ->copyToStatsdAt( "$wiki.growthexperiments.tasktypecount.$taskTypeId" )
156                ->incrementBy( $taskTypeCount );
157        }
158    }
159
160    /**
161     * @param string[] $taskTypes List of task type IDs to count for
162     * @param string[] $topics List of topic IDs to count for
163     * @param int[][] $taskCounts task type ID => topic ID => count
164     * @param int[] $taskTypeCounts task type ID => total count
165     * @param int[] $topicCounts topic ID => total count
166     */
167    private function printResults( $taskTypes, $topics, array $taskCounts, array $taskTypeCounts, array $topicCounts ) {
168        $output = $this->getOption( 'output', 'ascii-table' );
169        if ( $output === 'none' ) {
170            return;
171        } elseif ( $output === 'json' ) {
172            $this->output( FormatJson::encode( [
173                'taskCounts' => $taskCounts,
174                'taskTypeCounts' => $taskTypeCounts,
175                'topicCounts' => $topicCounts,
176            ], false, FormatJson::UTF8_OK ) );
177            return;
178        }
179
180        // Output header
181        $this->output( str_pad( 'Topic', 25, ' ' ) . ' ' );
182        foreach ( $taskTypes as $taskType ) {
183            $this->output( str_pad( $taskType, 10, ' ' ) . ' ' );
184        }
185        $this->output( "\n" . str_repeat( '-', 80 ) . "\n" );
186
187        foreach ( $topics as $topic ) {
188            $this->output( str_pad( $topic, 25, ' ' ) . ' ' );
189            foreach ( $taskTypes as $taskType ) {
190                $this->output( str_pad( (string)$taskCounts[$taskType][$topic], 3, ' ', STR_PAD_RIGHT )
191                               . str_repeat( ' ', 8 ) );
192            }
193            $this->output( "\n" );
194        }
195    }
196
197}
198
199$maintClass = ListTaskCounts::class;
200require_once RUN_MAINTENANCE_IF_MAIN;