Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
47.42% |
46 / 97 |
|
40.00% |
2 / 5 |
CRAP | |
0.00% |
0 / 1 |
CampaignStats | |
47.42% |
46 / 97 |
|
40.00% |
2 / 5 |
59.00 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getStatsForRecord | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getStatsForRecords | |
80.39% |
41 / 51 |
|
0.00% |
0 / 1 |
9.61 | |||
getUploadedMedia | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
12 | |||
getSummaryCounts | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\MediaUploader\Campaign; |
4 | |
5 | use MediaWiki\Extension\MediaUploader\Config\RawConfig; |
6 | use Title; |
7 | use WANObjectCache; |
8 | use Wikimedia\Rdbms\Database; |
9 | use Wikimedia\Rdbms\IDatabase; |
10 | use Wikimedia\Rdbms\ILoadBalancer; |
11 | |
12 | /** |
13 | * Facility for retrieving statistics about campaigns. |
14 | */ |
15 | class CampaignStats { |
16 | |
17 | /** @var ILoadBalancer */ |
18 | private $loadBalancer; |
19 | |
20 | /** @var WANObjectCache */ |
21 | private $cache; |
22 | |
23 | /** @var RawConfig */ |
24 | private $rawConfig; |
25 | |
26 | /** |
27 | * CampaignStats constructor. |
28 | * |
29 | * @param ILoadBalancer $loadBalancer |
30 | * @param WANObjectCache $cache |
31 | * @param RawConfig $rawConfig |
32 | * |
33 | * @internal Only for use by ServiceWiring |
34 | */ |
35 | public function __construct( |
36 | ILoadBalancer $loadBalancer, |
37 | WANObjectCache $cache, |
38 | RawConfig $rawConfig |
39 | ) { |
40 | $this->loadBalancer = $loadBalancer; |
41 | $this->cache = $cache; |
42 | $this->rawConfig = $rawConfig; |
43 | } |
44 | |
45 | /** |
46 | * Get stats for a single record. |
47 | * @see CampaignStats::getStatsForRecords() |
48 | * |
49 | * @param CampaignRecord $record |
50 | * |
51 | * @return array|null stats record or null on failure |
52 | */ |
53 | public function getStatsForRecord( CampaignRecord $record ): ?array { |
54 | $stats = $this->getStatsForRecords( [ $record ] ); |
55 | return $stats[$record->getPageId() ?: -1] ?? null; |
56 | } |
57 | |
58 | /** |
59 | * The records must have their titles populated. Otherwise, this will |
60 | * throw an exception. |
61 | * |
62 | * @param CampaignRecord[] $records |
63 | * |
64 | * @return array Map: campaign ID => stats array. |
65 | * Keys: 'trackingCategory': string (DB key) |
66 | * 'uploadedMediaCount': int |
67 | * 'contributorsCount': int |
68 | * 'uploadedMedia': string[] (DB keys of max. 24 files) |
69 | */ |
70 | public function getStatsForRecords( array $records ): array { |
71 | $recordMap = []; |
72 | foreach ( $records as $record ) { |
73 | // Note: the page ID won't actually ever be null, but it's hard to |
74 | // convince Phan to this. |
75 | $recordMap[$record->getPageId() ?: -1] = $record; |
76 | } |
77 | |
78 | $cache = $this->cache; |
79 | $keys = $cache->makeMultiKeys( |
80 | array_keys( $recordMap ), |
81 | static function ( $id ) use ( $cache ) { |
82 | return $cache->makeKey( |
83 | 'mediauploader', |
84 | 'campaign-stats', |
85 | $id |
86 | ); |
87 | } |
88 | ); |
89 | |
90 | $fromCache = $cache->getMultiWithUnionSetCallback( |
91 | $keys, |
92 | $this->rawConfig->getSetting( 'campaignStatsMaxAge' ), |
93 | function ( array $ids, array &$ttls, array &$setOpts ) use ( $recordMap ) { |
94 | $db = $this->loadBalancer->getConnection( DB_REPLICA ); |
95 | $setOpts += Database::getCacheSetOptions( $db ); |
96 | |
97 | // Construct a tracking category => id map |
98 | $catToId = []; |
99 | // id => tracking category or null |
100 | $idToCat = []; |
101 | foreach ( $ids as $id ) { |
102 | $catTitle = Title::newFromText( |
103 | $recordMap[$id]->getTrackingCategoryName( $this->rawConfig ), |
104 | NS_CATEGORY |
105 | ); |
106 | |
107 | if ( $catTitle === null ) { |
108 | $idToCat[$id] = null; |
109 | } else { |
110 | $idToCat[$id] = $catTitle->getDBkey(); |
111 | $catToId[$catTitle->getDBkey()] = $id; |
112 | } |
113 | } |
114 | |
115 | // Do the batch queries |
116 | if ( $catToId ) { |
117 | $summary = $this->getSummaryCounts( $db, $catToId ); |
118 | $media = $this->getUploadedMedia( $db, $catToId ); |
119 | } |
120 | |
121 | // Aggregate results |
122 | $toCache = []; |
123 | foreach ( $idToCat as $id => $category ) { |
124 | if ( $category === null ) { |
125 | $toCache[$id] = null; |
126 | } else { |
127 | $toCache[$id] = [ |
128 | 'trackingCategory' => $category, |
129 | 'uploadedMediaCount' => $summary[$id][0] ?? 0, |
130 | 'contributorsCount' => $summary[$id][1] ?? 0, |
131 | 'uploadedMedia' => $media[$id] ?? [], |
132 | ]; |
133 | } |
134 | } |
135 | |
136 | return $toCache; |
137 | } |
138 | ); |
139 | |
140 | $result = []; |
141 | foreach ( $fromCache as $key => $item ) { |
142 | $result[$keys[$key]] = $item; |
143 | } |
144 | return $result; |
145 | } |
146 | |
147 | /** |
148 | * @param IDatabase $db |
149 | * @param int[] $categories Map: campaign DB key => campaign page ID |
150 | * |
151 | * @return string[][] campaign ID => string[], the strings are filenames |
152 | */ |
153 | private function getUploadedMedia( IDatabase $db, array $categories ): array { |
154 | $result = $db->newSelectQueryBuilder() |
155 | ->table( 'categorylinks' ) |
156 | ->fields( [ 'cl_to', 'page_namespace', 'page_title' ] ) |
157 | ->where( [ |
158 | 'cl_to' => array_keys( $categories ), |
159 | 'cl_type' => 'file', |
160 | ] ) |
161 | ->join( 'page', null, 'cl_from=page_id' ) |
162 | ->orderBy( 'cl_timestamp', 'DESC' ) |
163 | ->useIndex( [ 'categorylinks' => 'cl_timestamp' ] ) |
164 | // Old, arbitrary limit. Seems fine. |
165 | ->limit( 24 ) |
166 | ->fetchResultSet(); |
167 | |
168 | $grouped = []; |
169 | foreach ( $result as $row ) { |
170 | $key = $categories[$row->cl_to]; |
171 | |
172 | if ( array_key_exists( $key, $grouped ) ) { |
173 | $grouped[$key][] = $row->page_title; |
174 | } else { |
175 | $grouped[$key] = [ $row->page_title ]; |
176 | } |
177 | } |
178 | |
179 | return $grouped; |
180 | } |
181 | |
182 | /** |
183 | * @param IDatabase $db |
184 | * @param int[] $categories Map: campaign DB key => campaign page ID |
185 | * |
186 | * @return array Map: campaign ID => [ media count, contributor count ] |
187 | */ |
188 | private function getSummaryCounts( IDatabase $db, array $categories ): array { |
189 | $result = $db->newSelectQueryBuilder() |
190 | ->table( 'categorylinks' ) |
191 | ->field( 'cl_to' ) |
192 | ->field( 'COUNT(DISTINCT img_actor)', 'contributors' ) |
193 | ->field( 'COUNT(cl_from)', 'media' ) |
194 | ->where( [ |
195 | 'cl_to' => array_keys( $categories ), |
196 | 'cl_type' => 'file', |
197 | ] ) |
198 | ->join( 'page', null, 'cl_from=page_id' ) |
199 | ->join( 'image', null, 'page_title=img_name' ) |
200 | ->groupBy( 'cl_to' ) |
201 | ->useIndex( [ 'categorylinks' => 'cl_timestamp' ] ) |
202 | ->fetchResultSet(); |
203 | |
204 | $map = []; |
205 | foreach ( $result as $row ) { |
206 | $map[$categories[$row->cl_to]] = [ |
207 | intval( $row->media ), |
208 | intval( $row->contributors ), |
209 | ]; |
210 | } |
211 | return $map; |
212 | } |
213 | } |