Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 215
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
Translation
0.00% covered (danger)
0.00%
0 / 215
0.00% covered (danger)
0.00%
0 / 16
1190
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isNew
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setIsNew
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStats
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDraftStats
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
6
 getPublishedStats
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
6
 getDeletionTrend
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
30
 getTrendByStatus
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
72
 getTranslationId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSourceTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSourceLanguage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTargetLanguage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPublishedCondition
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getAllPublishedTranslations
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
30
 newFromRow
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace ContentTranslation;
4
5use MediaWiki\MediaWikiServices;
6use MediaWiki\Storage\NameTableAccessException;
7use Wikimedia\Rdbms\IDatabase;
8
9class 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}