Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 179
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Translation
0.00% covered (danger)
0.00%
0 / 179
0.00% covered (danger)
0.00%
0 / 14
992
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 / 28
0.00% covered (danger)
0.00%
0 / 1
6
 getDeletionTrend
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
30
 getTrendByStatus
0.00% covered (danger)
0.00%
0 / 46
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 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getAllPublishedTranslations
0.00% covered (danger)
0.00%
0 / 35
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;
8use Wikimedia\Rdbms\IExpression;
9
10class 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}