Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 215 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 1 |
Translation | |
0.00% |
0 / 215 |
|
0.00% |
0 / 16 |
1190 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isNew | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setIsNew | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getStats | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDraftStats | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
6 | |||
getPublishedStats | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
6 | |||
getDeletionTrend | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
30 | |||
getTrendByStatus | |
0.00% |
0 / 50 |
|
0.00% |
0 / 1 |
72 | |||
getTranslationId | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSourceTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSourceLanguage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTargetLanguage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPublishedCondition | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getAllPublishedTranslations | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
30 | |||
newFromRow | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace ContentTranslation; |
4 | |
5 | use MediaWiki\MediaWikiServices; |
6 | use MediaWiki\Storage\NameTableAccessException; |
7 | use Wikimedia\Rdbms\IDatabase; |
8 | |
9 | class Translation { |
10 | private bool $isNew = false; |
11 | |
12 | /** @var array */ |
13 | public $translation; |
14 | |
15 | public function __construct( $translation ) { |
16 | $this->translation = $translation; |
17 | } |
18 | |
19 | /** |
20 | * @return bool Whether the last CRUD operation on this translation was "create" |
21 | */ |
22 | public function isNew(): bool { |
23 | return $this->isNew; |
24 | } |
25 | |
26 | public function setIsNew( bool $isNew ): void { |
27 | $this->isNew = $isNew; |
28 | } |
29 | |
30 | /** |
31 | * Get the stats for all translations in draft or published status. |
32 | * @return array |
33 | */ |
34 | public static function getStats() { |
35 | return array_merge( self::getDraftStats(), self::getPublishedStats() ); |
36 | } |
37 | |
38 | /** |
39 | * Get the stats for all translations in draft status and not having |
40 | * any published URL. |
41 | * If the translation is with draft status and has a target_url it |
42 | * was published atleast once. |
43 | * @return array |
44 | */ |
45 | public static function getDraftStats() { |
46 | $lb = MediaWikiServices::getInstance()->getService( 'ContentTranslation.LoadBalancer' ); |
47 | $dbr = $lb->getConnection( DB_REPLICA ); |
48 | |
49 | $rows = $dbr->select( |
50 | 'cx_translations', |
51 | [ |
52 | 'sourceLanguage' => 'translation_source_language', |
53 | 'targetLanguage' => 'translation_target_language', |
54 | 'status' => 'translation_status', |
55 | 'count' => 'COUNT(*)', |
56 | 'translators' => 'COUNT(DISTINCT translation_started_by)', |
57 | ], |
58 | [ |
59 | 'translation_status' => 'draft', |
60 | 'translation_target_url' => null, |
61 | ], |
62 | __METHOD__, |
63 | [ |
64 | 'GROUP BY' => [ |
65 | 'translation_source_language', |
66 | 'translation_target_language', |
67 | ], |
68 | ] |
69 | ); |
70 | |
71 | $result = []; |
72 | |
73 | foreach ( $rows as $row ) { |
74 | $result[] = (array)$row; |
75 | } |
76 | |
77 | return $result; |
78 | } |
79 | |
80 | /** |
81 | * Get the stats for all translations in published status or having |
82 | * a published URL. |
83 | * If the translation has a target_url it was published atleast once. |
84 | * @return array |
85 | */ |
86 | public static function getPublishedStats() { |
87 | $lb = MediaWikiServices::getInstance()->getService( 'ContentTranslation.LoadBalancer' ); |
88 | $dbr = $lb->getConnection( DB_REPLICA ); |
89 | |
90 | $rows = $dbr->select( |
91 | 'cx_translations', |
92 | [ |
93 | 'sourceLanguage' => 'translation_source_language', |
94 | 'targetLanguage' => 'translation_target_language', |
95 | // A published translation can be in deleted state too. But for the purpose |
96 | // of stats, it should be counted as published. 'deleted' here just means |
97 | // the soft deletion of entry from CX tables. Not the article deletion. |
98 | // For this, use hard coded quoted value 'published' as status. |
99 | "'published' as status", |
100 | 'count' => 'COUNT(*)', |
101 | 'translators' => 'COUNT(DISTINCT translation_started_by)', |
102 | ], |
103 | self::getPublishedCondition( $dbr ), |
104 | __METHOD__, |
105 | [ |
106 | 'GROUP BY' => [ |
107 | 'translation_source_language', |
108 | 'translation_target_language', |
109 | ], |
110 | ] |
111 | ); |
112 | |
113 | $result = []; |
114 | foreach ( $rows as $row ) { |
115 | $result[] = (array)$row; |
116 | } |
117 | |
118 | return $result; |
119 | } |
120 | |
121 | /** |
122 | * Get time-wise cumulative number of deletions for given |
123 | * language pairs, with given interval. |
124 | * |
125 | * @param string $interval |
126 | * @return array<string,array> |
127 | */ |
128 | public static function getDeletionTrend( $interval ): array { |
129 | $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); |
130 | $dbr = $lb->getConnection( DB_REPLICA ); |
131 | |
132 | $conditions = [ |
133 | 'ar_rev_id = ct_rev_id' |
134 | ]; |
135 | |
136 | $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); |
137 | try { |
138 | $conditions['ct_tag_id'] = $changeTagDefStore->getId( 'contenttranslation' ); |
139 | } catch ( NameTableAccessException $exception ) { |
140 | // No translations published yet, so can skip query |
141 | return []; |
142 | } |
143 | |
144 | $options = []; |
145 | if ( $interval === 'week' ) { |
146 | $options = [ |
147 | 'GROUP BY' => [ |
148 | 'YEARWEEK(ar_timestamp, 3)', |
149 | ], |
150 | ]; |
151 | } elseif ( $interval === 'month' ) { |
152 | $options = [ |
153 | 'GROUP BY' => [ |
154 | 'YEAR(ar_timestamp)', |
155 | 'MONTH(ar_timestamp)', |
156 | ], |
157 | ]; |
158 | } |
159 | |
160 | $rows = $dbr->select( |
161 | [ 'change_tag', 'archive' ], |
162 | [ 'ar_timestamp', 'count' => 'COUNT(ar_page_id)' ], |
163 | $conditions, |
164 | __METHOD__, |
165 | $options |
166 | ); |
167 | |
168 | $count = 0; |
169 | $result = []; |
170 | $dm = new DateManipulator( $interval ); |
171 | foreach ( $rows as $row ) { |
172 | $count += (int)$row->count; |
173 | $time = $dm->getIntervalIdentifier( $row->ar_timestamp )->format( 'U' ); |
174 | $result[$time] = [ |
175 | 'count' => $count, |
176 | 'delta' => (int)$row->count, |
177 | ]; |
178 | } |
179 | |
180 | return $result; |
181 | } |
182 | |
183 | /** |
184 | * Get time-wise cumulative number of translations for given |
185 | * language pairs, with given interval. |
186 | * |
187 | * @param string|null $source Source language code |
188 | * @param string|null $target Target language code |
189 | * @param string $status Status of translation. Either 'published' or 'draft' |
190 | * @param string $interval 'weekly' or 'monthly' trend |
191 | * @param int|null $translatorId |
192 | * @return array |
193 | */ |
194 | public static function getTrendByStatus( |
195 | $source, $target, $status, $interval, $translatorId |
196 | ) { |
197 | $lb = MediaWikiServices::getInstance()->getService( 'ContentTranslation.LoadBalancer' ); |
198 | $dbr = $lb->getConnection( DB_REPLICA ); |
199 | |
200 | $conditions = []; |
201 | if ( $status === 'published' ) { |
202 | $conditions[] = self::getPublishedCondition( $dbr ); |
203 | } else { |
204 | $conditions[] = $dbr->makeList( |
205 | [ |
206 | 'translation_status' => 'draft', |
207 | 'translation_target_url' => null, |
208 | ], |
209 | LIST_AND |
210 | ); |
211 | } |
212 | |
213 | if ( $source !== null ) { |
214 | $conditions['translation_source_language'] = $source; |
215 | } |
216 | if ( $target !== null ) { |
217 | $conditions['translation_target_language'] = $target; |
218 | } |
219 | if ( $translatorId !== null ) { |
220 | $conditions['translation_last_update_by'] = $translatorId; |
221 | } |
222 | $options = []; |
223 | if ( $interval === 'week' ) { |
224 | $options = [ |
225 | 'GROUP BY' => [ |
226 | 'YEARWEEK(translation_last_updated_timestamp, 3)', |
227 | ], |
228 | ]; |
229 | } elseif ( $interval === 'month' ) { |
230 | $options = [ |
231 | 'GROUP BY' => [ |
232 | 'YEAR(translation_last_updated_timestamp)', |
233 | 'MONTH(translation_last_updated_timestamp)', |
234 | ], |
235 | ]; |
236 | } |
237 | |
238 | $rows = $dbr->select( |
239 | [ 'cx_translations' ], |
240 | [ 'date' => 'translation_last_updated_timestamp', 'count' => 'COUNT(translation_id)' ], |
241 | $dbr->makeList( $conditions, LIST_AND ), |
242 | __METHOD__, |
243 | $options |
244 | ); |
245 | |
246 | $count = 0; |
247 | $result = []; |
248 | $dm = new DateManipulator( $interval ); |
249 | foreach ( $rows as $row ) { |
250 | $count += (int)$row->count; |
251 | $time = $dm->getIntervalIdentifier( $row->date )->format( 'U' ); |
252 | $result[$time] = [ |
253 | 'count' => $count, |
254 | 'delta' => (int)$row->count, |
255 | ]; |
256 | } |
257 | |
258 | return $result; |
259 | } |
260 | |
261 | public function getTranslationId() { |
262 | return $this->translation['id']; |
263 | } |
264 | |
265 | public function getSourceTitle(): string { |
266 | return $this->translation['sourceTitle']; |
267 | } |
268 | |
269 | public function getSourceLanguage(): string { |
270 | return $this->translation['sourceLanguage']; |
271 | } |
272 | |
273 | public function getTargetLanguage(): string { |
274 | return $this->translation['targetLanguage']; |
275 | } |
276 | |
277 | /** |
278 | * Return the underlying data fields. |
279 | * |
280 | * @return array |
281 | */ |
282 | public function getData() { |
283 | return $this->translation; |
284 | } |
285 | |
286 | public static function getPublishedCondition( IDatabase $db ) { |
287 | return $db->makeList( |
288 | [ |
289 | 'translation_status' => 'published', |
290 | 'translation_target_url IS NOT NULL', |
291 | ], |
292 | LIST_OR |
293 | ); |
294 | } |
295 | |
296 | /** |
297 | * Get all published translation records. |
298 | * |
299 | * @param string $from Source language code |
300 | * @param string $to Target language code |
301 | * @param int $limit Number of records to fetch atmost |
302 | * @param int $offset Offset from which at most $limit records to fetch |
303 | * @return array |
304 | */ |
305 | public static function getAllPublishedTranslations( $from, $to, $limit, $offset ) { |
306 | $lb = MediaWikiServices::getInstance()->getService( 'ContentTranslation.LoadBalancer' ); |
307 | $dbr = $lb->getConnection( DB_REPLICA ); |
308 | $conditions = []; |
309 | $conditions[] = self::getPublishedCondition( $dbr ); |
310 | |
311 | if ( $from ) { |
312 | $conditions['translation_source_language'] = $from; |
313 | } |
314 | |
315 | if ( $to ) { |
316 | $conditions['translation_target_language'] = $to; |
317 | } |
318 | |
319 | $options = [ 'LIMIT' => $limit ]; |
320 | |
321 | if ( $offset ) { |
322 | $options['OFFSET'] = $offset; |
323 | } |
324 | |
325 | $rows = $dbr->select( |
326 | 'cx_translations', |
327 | [ |
328 | 'translationId' => 'translation_id', |
329 | 'sourceTitle' => 'translation_source_title', |
330 | 'targetTitle' => 'translation_target_title', |
331 | 'sourceLanguage' => 'translation_source_language', |
332 | 'sourceRevisionId' => 'translation_source_revision_id', |
333 | 'targetRevisionId' => 'translation_target_revision_id', |
334 | 'targetLanguage' => 'translation_target_language', |
335 | 'sourceURL' => 'translation_source_url', |
336 | 'targetURL' => 'translation_target_url', |
337 | 'publishedDate' => 'translation_last_updated_timestamp', |
338 | 'stats' => 'translation_progress', |
339 | ], |
340 | $conditions, |
341 | __METHOD__, |
342 | $options |
343 | ); |
344 | |
345 | $result = []; |
346 | |
347 | foreach ( $rows as $row ) { |
348 | $translation = (array)$row; |
349 | $translation['stats'] = json_decode( $translation['stats'] ); |
350 | $result[] = $translation; |
351 | } |
352 | |
353 | return $result; |
354 | } |
355 | |
356 | /** |
357 | * @param \stdClass $row |
358 | * @return Translation |
359 | */ |
360 | public static function newFromRow( $row ) { |
361 | $fields = [ |
362 | 'id' => (int)$row->translation_id, |
363 | 'sourceTitle' => $row->translation_source_title, |
364 | 'targetTitle' => $row->translation_target_title, |
365 | 'sourceLanguage' => $row->translation_source_language, |
366 | 'targetLanguage' => $row->translation_target_language, |
367 | 'sourceRevisionId' => $row->translation_source_revision_id, |
368 | 'targetRevisionId' => $row->translation_target_revision_id, |
369 | 'sourceURL' => $row->translation_source_url, |
370 | 'targetURL' => $row->translation_target_url, |
371 | 'status' => $row->translation_status, |
372 | 'startTimestamp' => $row->translation_start_timestamp, |
373 | 'lastUpdateTimestamp' => $row->translation_last_updated_timestamp, |
374 | 'progress' => $row->translation_progress, |
375 | 'startedTranslator' => $row->translation_started_by, |
376 | 'lastUpdatedTranslator' => $row->translation_last_update_by, |
377 | 'cxVersion' => 1, |
378 | ]; |
379 | |
380 | // BC code to gracefully handle lack of schema change |
381 | if ( isset( $row->translation_cx_version ) ) { |
382 | $fields['cxVersion'] = (int)$row->translation_cx_version; |
383 | } |
384 | |
385 | return new Translation( $fields ); |
386 | } |
387 | } |