Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 187 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
SectionTranslationStore | |
0.00% |
0 / 187 |
|
0.00% |
0 / 14 |
992 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
insertTranslation | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
insertMultipleTranslations | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
updateTranslation | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
updateTranslationStatusById | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
findTranslation | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
findTranslationBySectionTitle | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
createTranslationFromRow | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getStatusIndexByStatus | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
doFindTranslationsByUser | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
30 | |||
findDraftSectionTranslationsByUser | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
12 | |||
findPublishedSectionTranslationsByUser | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
42 | |||
deleteTranslationById | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
translationToDBRow | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | declare( strict_types = 1 ); |
4 | |
5 | namespace ContentTranslation\Store; |
6 | |
7 | use ContentTranslation\DTO\DraftTranslationDTO; |
8 | use ContentTranslation\DTO\PublishedSectionTranslationDTO; |
9 | use ContentTranslation\DTO\PublishedTranslationDTO; |
10 | use ContentTranslation\Entity\SectionTranslation; |
11 | use ContentTranslation\LoadBalancer; |
12 | use InvalidArgumentException; |
13 | use Wikimedia\Rdbms\IDatabase; |
14 | use Wikimedia\Rdbms\IResultWrapper; |
15 | use Wikimedia\Rdbms\Platform\ISQLPlatform; |
16 | use Wikimedia\Rdbms\SelectQueryBuilder; |
17 | |
18 | class SectionTranslationStore { |
19 | public const TABLE_NAME = 'cx_section_translations'; |
20 | public const TRANSLATION_STATUS_DRAFT = 'draft'; |
21 | public const TRANSLATION_STATUS_PUBLISHED = 'published'; |
22 | public const TRANSLATION_STATUS_DELETED = 'deleted'; |
23 | |
24 | /** |
25 | * This constant contains the mappings of translation statuses to integers, |
26 | * that are used as values for "cxsx_translation_status" field inside the database. |
27 | * This constant should NOT be changed or reordered. |
28 | */ |
29 | public const TRANSLATION_STATUSES = [ |
30 | 0 => self::TRANSLATION_STATUS_DRAFT, |
31 | 1 => self::TRANSLATION_STATUS_PUBLISHED, |
32 | 2 => self::TRANSLATION_STATUS_DELETED, |
33 | ]; |
34 | |
35 | /** @var LoadBalancer */ |
36 | private $lb; |
37 | |
38 | public function __construct( LoadBalancer $loadBalancer ) { |
39 | $this->lb = $loadBalancer; |
40 | } |
41 | |
42 | public function insertTranslation( SectionTranslation $translation ) { |
43 | $dbw = $this->lb->getConnection( DB_PRIMARY ); |
44 | $values = $this->translationToDBRow( $translation ); |
45 | // set start/last_updated timestamps to current timestamp |
46 | $values['cxsx_translation_start_timestamp'] = $dbw->timestamp(); |
47 | $values['cxsx_translation_last_updated_timestamp'] = $dbw->timestamp(); |
48 | |
49 | $dbw->newInsertQueryBuilder() |
50 | ->insertInto( self::TABLE_NAME ) |
51 | ->row( $values ) |
52 | ->caller( __METHOD__ ) |
53 | ->execute(); |
54 | $translation->setId( $dbw->insertId() ); |
55 | } |
56 | |
57 | /** |
58 | * @param SectionTranslation[] $translations |
59 | * @return void |
60 | */ |
61 | public function insertMultipleTranslations( array $translations ): void { |
62 | if ( !$translations ) { |
63 | return; |
64 | } |
65 | |
66 | $dbw = $this->lb->getConnection( DB_PRIMARY ); |
67 | $rows = []; |
68 | foreach ( $translations as $translation ) { |
69 | $values = $this->translationToDBRow( $translation ); |
70 | // set start/last_updated timestamps to current timestamp |
71 | $values['cxsx_translation_start_timestamp'] = $dbw->timestamp(); |
72 | $values['cxsx_translation_last_updated_timestamp'] = $dbw->timestamp(); |
73 | $rows[] = $values; |
74 | } |
75 | |
76 | $dbw->newInsertQueryBuilder() |
77 | ->insertInto( self::TABLE_NAME ) |
78 | ->rows( $rows ) |
79 | ->caller( __METHOD__ ) |
80 | ->execute(); |
81 | } |
82 | |
83 | public function updateTranslation( SectionTranslation $translation ) { |
84 | $dbw = $this->lb->getConnection( DB_PRIMARY ); |
85 | $values = $this->translationToDBRow( $translation ); |
86 | $values['cxsx_translation_last_updated_timestamp'] = $dbw->timestamp(); |
87 | |
88 | $dbw->newUpdateQueryBuilder() |
89 | ->update( self::TABLE_NAME ) |
90 | ->set( $values ) |
91 | ->where( [ 'cxsx_id' => $translation->getId() ] ) |
92 | ->caller( __METHOD__ ) |
93 | ->execute(); |
94 | } |
95 | |
96 | /** |
97 | * @param int|null $id The "cxsx_id" of the section translation row |
98 | * @param int|null $translationStatus Either "deleted", "draft" or "published" |
99 | */ |
100 | public function updateTranslationStatusById( ?int $id, ?int $translationStatus ) { |
101 | $dbw = $this->lb->getConnection( DB_PRIMARY ); |
102 | $dbw->newUpdateQueryBuilder() |
103 | ->update( self::TABLE_NAME ) |
104 | ->set( [ 'cxsx_translation_status' => $translationStatus ] ) |
105 | ->where( [ 'cxsx_id' => $id ] ) |
106 | ->caller( __METHOD__ ) |
107 | ->execute(); |
108 | } |
109 | |
110 | public function findTranslation( int $translationId, string $sectionId ): ?SectionTranslation { |
111 | $dbr = $this->lb->getConnection( DB_REPLICA ); |
112 | |
113 | $row = $dbr->newSelectQueryBuilder() |
114 | ->select( IDatabase::ALL_ROWS ) |
115 | ->from( self::TABLE_NAME ) |
116 | ->where( [ |
117 | 'cxsx_translation_id' => $translationId, |
118 | 'cxsx_section_id' => $sectionId |
119 | ] ) |
120 | ->caller( __METHOD__ ) |
121 | ->fetchRow(); |
122 | return $row ? $this->createTranslationFromRow( $row ) : null; |
123 | } |
124 | |
125 | /** |
126 | * @param int $translationId The id of the parent translation (from "cx_translations" table) |
127 | * @param string $sectionTitle The source section title |
128 | * @return SectionTranslation|null |
129 | */ |
130 | public function findTranslationBySectionTitle( int $translationId, string $sectionTitle ): ?SectionTranslation { |
131 | $dbr = $this->lb->getConnection( DB_REPLICA ); |
132 | |
133 | $row = $dbr->newSelectQueryBuilder() |
134 | ->select( ISQLPlatform::ALL_ROWS ) |
135 | ->from( self::TABLE_NAME ) |
136 | ->where( [ |
137 | 'cxsx_translation_id' => $translationId, |
138 | 'cxsx_source_section_title' => $sectionTitle |
139 | ] ) |
140 | ->limit( 1 ) |
141 | ->caller( __METHOD__ ) |
142 | ->fetchRow(); |
143 | |
144 | return $row ? $this->createTranslationFromRow( $row ) : null; |
145 | } |
146 | |
147 | public function createTranslationFromRow( \stdClass $row ): SectionTranslation { |
148 | return new SectionTranslation( |
149 | (int)$row->cxsx_id, |
150 | (int)$row->cxsx_translation_id, |
151 | $row->cxsx_section_id, |
152 | $row->cxsx_source_section_title, |
153 | $row->cxsx_target_section_title, |
154 | $row->cxsx_translation_status ? (int)$row->cxsx_translation_status : null, |
155 | $row->cxsx_translation_progress, |
156 | ); |
157 | } |
158 | |
159 | public static function getStatusIndexByStatus( string $status ) { |
160 | $index = array_search( $status, self::TRANSLATION_STATUSES ); |
161 | |
162 | if ( $index === false ) { |
163 | throw new InvalidArgumentException( '[CX] Invalid status provided' ); |
164 | } |
165 | |
166 | return $index; |
167 | } |
168 | |
169 | /** |
170 | * @param int $userId User ID |
171 | * @param string|null $from |
172 | * @param string|null $to |
173 | * @param string|null $status The status of the translation. Either "draft" or "published" |
174 | * @param int $limit How many results to return. Defaults to 100, same as for the "contenttranslation" list API |
175 | * @param string|null $offset Offset condition (timestamp) |
176 | * @return IResultWrapper |
177 | */ |
178 | private function doFindTranslationsByUser( |
179 | int $userId, |
180 | string $from = null, |
181 | string $to = null, |
182 | string $status = null, |
183 | int $limit = 100, |
184 | string $offset = null |
185 | ): IResultWrapper { |
186 | // Note: there is no index on translation_last_updated_timestamp |
187 | $dbr = $this->lb->getConnection( DB_REPLICA ); |
188 | |
189 | $whereConditions = [ 'translation_started_by' => $userId ]; |
190 | |
191 | if ( $status !== null ) { |
192 | $statusIndex = self::getStatusIndexByStatus( $status ); |
193 | $isPublishedCondition = $dbr->makeList( |
194 | [ |
195 | 'translation_status' => $status, |
196 | 'cxsx_translation_status' => $statusIndex |
197 | ], |
198 | LIST_OR |
199 | ); |
200 | $whereConditions[] = $isPublishedCondition; |
201 | } |
202 | if ( $from !== null ) { |
203 | $whereConditions['translation_source_language'] = $from; |
204 | } |
205 | if ( $to !== null ) { |
206 | $whereConditions['translation_target_language'] = $to; |
207 | } |
208 | if ( $offset !== null ) { |
209 | $ts = $dbr->addQuotes( $dbr->timestamp( $offset ) ); |
210 | $whereConditions[] = "translation_last_updated_timestamp < $ts"; |
211 | } |
212 | |
213 | return $dbr->newSelectQueryBuilder() |
214 | ->select( ISQLPlatform::ALL_ROWS ) |
215 | ->from( TranslationStore::TRANSLATION_TABLE_NAME ) |
216 | ->leftJoin( self::TABLE_NAME, null, 'translation_id = cxsx_translation_id' ) |
217 | ->where( $whereConditions ) |
218 | ->orderBy( 'translation_last_updated_timestamp', SelectQueryBuilder::SORT_DESC ) |
219 | ->limit( $limit ) |
220 | ->caller( __METHOD__ ) |
221 | ->fetchResultSet(); |
222 | } |
223 | |
224 | /** |
225 | * @param int $userId User ID |
226 | * @param string|null $from |
227 | * @param string|null $to |
228 | * @param int $limit How many results to return. Defaults to 100, same as for the "contenttranslation" list API |
229 | * @param string|null $offset Offset condition (timestamp) |
230 | * @return DraftTranslationDTO[] |
231 | */ |
232 | public function findDraftSectionTranslationsByUser( |
233 | int $userId, |
234 | string $from = null, |
235 | string $to = null, |
236 | int $limit = 100, |
237 | string $offset = null |
238 | ): array { |
239 | $resultSet = $this->doFindTranslationsByUser( |
240 | $userId, |
241 | $from, |
242 | $to, |
243 | self::TRANSLATION_STATUS_DRAFT, |
244 | $limit, |
245 | $offset |
246 | ); |
247 | |
248 | $result = []; |
249 | foreach ( $resultSet as $row ) { |
250 | $result[] = new DraftTranslationDTO( |
251 | $row->cxsx_id ? (int)$row->cxsx_id : null, |
252 | (int)$row->translation_id, |
253 | $row->cxsx_section_id ?? null, |
254 | $row->translation_source_title, |
255 | $row->translation_source_language, |
256 | $row->translation_target_language, |
257 | $row->translation_start_timestamp, |
258 | $row->translation_last_updated_timestamp, |
259 | self::TRANSLATION_STATUSES[ $row->cxsx_translation_status ] ?? $row->translation_status, |
260 | $row->translation_source_revision_id, |
261 | $row->cxsx_translation_progress ?? $row->translation_progress, |
262 | $row->translation_target_title, |
263 | $row->cxsx_source_section_title, |
264 | $row->cxsx_target_section_title, |
265 | ); |
266 | } |
267 | |
268 | return $result; |
269 | } |
270 | |
271 | /** |
272 | * @param int $userId User ID |
273 | * @param string|null $from |
274 | * @param string|null $to |
275 | * @param int $limit How many results to return. Defaults to 100, same as for the "contenttranslation" list API |
276 | * @param string|null $offset Offset condition (timestamp) |
277 | * @return PublishedTranslationDTO[] |
278 | */ |
279 | public function findPublishedSectionTranslationsByUser( |
280 | int $userId, |
281 | string $from = null, |
282 | string $to = null, |
283 | int $limit = 100, |
284 | string $offset = null |
285 | ): array { |
286 | $resultSet = $this->doFindTranslationsByUser( |
287 | $userId, |
288 | $from, |
289 | $to, |
290 | self::TRANSLATION_STATUS_PUBLISHED, |
291 | $limit, |
292 | $offset |
293 | ); |
294 | |
295 | /** |
296 | * An array of PublishedSectionTranslationDTO objects grouped by translation id |
297 | * @var PublishedSectionTranslationDTO[] $publishedSectionTranslations |
298 | */ |
299 | $publishedSectionTranslations = []; |
300 | $publishedStatusIndex = self::getStatusIndexByStatus( self::TRANSLATION_STATUS_PUBLISHED ); |
301 | |
302 | foreach ( $resultSet as $row ) { |
303 | $translationId = (int)$row->translation_id; |
304 | if ( $row->cxsx_id && $row->cxsx_translation_status === $publishedStatusIndex ) { |
305 | $publishedSectionTranslations[$translationId] = new PublishedSectionTranslationDTO( |
306 | (int)$row->cxsx_id, |
307 | $row->cxsx_section_id, |
308 | $row->cxsx_translation_start_timestamp, |
309 | $row->cxsx_translation_last_updated_timestamp, |
310 | $row->cxsx_source_section_title, |
311 | $row->cxsx_target_section_title, |
312 | ); |
313 | } |
314 | } |
315 | |
316 | $result = []; |
317 | foreach ( $resultSet as $row ) { |
318 | $translationId = (int)$row->translation_id; |
319 | // multiple rows can exist for the same translation id |
320 | // we only need one DTO per translation |
321 | if ( isset( $result[$translationId] ) ) { |
322 | continue; |
323 | } |
324 | $result[$translationId] = new PublishedTranslationDTO( |
325 | $translationId, |
326 | $row->translation_source_title, |
327 | $row->translation_source_language, |
328 | $row->translation_target_language, |
329 | $row->translation_start_timestamp, |
330 | $row->translation_last_updated_timestamp, |
331 | $row->translation_source_revision_id, |
332 | $row->translation_target_title, |
333 | $row->translation_target_url, |
334 | $publishedSectionTranslations[$translationId] ?? [] |
335 | ); |
336 | } |
337 | |
338 | return array_values( $result ); |
339 | } |
340 | |
341 | /** |
342 | * Given the "parent" translation id and the section id |
343 | * (in the "${revision}_${sectionNumber} form), this method |
344 | * deletes the corresponding section translation from the |
345 | * "cx_section_translations" table. |
346 | * |
347 | * @param int $sectionTranslationId |
348 | * @return void |
349 | */ |
350 | public function deleteTranslationById( int $sectionTranslationId ): void { |
351 | $dbw = $this->lb->getConnection( DB_PRIMARY ); |
352 | |
353 | $deletedStatusIndex = self::getStatusIndexByStatus( self::TRANSLATION_STATUS_DELETED ); |
354 | $dbw->newUpdateQueryBuilder() |
355 | ->update( self::TABLE_NAME ) |
356 | ->set( [ 'cxsx_translation_status' => $deletedStatusIndex ] ) |
357 | ->where( [ 'cxsx_id' => $sectionTranslationId ] ) |
358 | ->caller( __METHOD__ ) |
359 | ->execute(); |
360 | } |
361 | |
362 | private function translationToDBRow( SectionTranslation $translation ): array { |
363 | return [ |
364 | 'cxsx_translation_id' => $translation->getTranslationId(), |
365 | 'cxsx_section_id' => $translation->getSectionId(), |
366 | 'cxsx_source_section_title' => $translation->getSourceSectionTitle(), |
367 | 'cxsx_target_section_title' => $translation->getTargetSectionTitle(), |
368 | 'cxsx_translation_status' => $translation->getTranslationStatus(), |
369 | 'cxsx_translation_progress' => $translation->getProgress(), |
370 | ]; |
371 | } |
372 | } |