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