Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 98 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
ListTaskCounts | |
0.00% |
0 / 92 |
|
0.00% |
0 / 6 |
462 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
20 | |||
getTaskTypesAndTopics | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getStats | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
30 | |||
reportTaskCounts | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
printResults | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\Maintenance; |
4 | |
5 | use GrowthExperiments\GrowthExperimentsServices; |
6 | use GrowthExperiments\NewcomerTasks\CachedSuggestionsInfo; |
7 | use GrowthExperiments\NewcomerTasks\ConfigurationLoader\ConfigurationLoader; |
8 | use GrowthExperiments\NewcomerTasks\ConfigurationLoader\TopicDecorator; |
9 | use GrowthExperiments\NewcomerTasks\SuggestionsInfo; |
10 | use MediaWiki\Json\FormatJson; |
11 | use MediaWiki\Maintenance\Maintenance; |
12 | use MediaWiki\WikiMap\WikiMap; |
13 | |
14 | $IP = getenv( 'MW_INSTALL_PATH' ); |
15 | if ( $IP === false ) { |
16 | $IP = __DIR__ . '/../../..'; |
17 | } |
18 | require_once "$IP/maintenance/Maintenance.php"; |
19 | |
20 | /** |
21 | * List the number of tasks available for each topic |
22 | */ |
23 | class 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; |
200 | require_once RUN_MAINTENANCE_IF_MAIN; |