Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 182 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
SectionTranslationStore | |
0.00% |
0 / 182 |
|
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 / 23 |
|
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 | $whereConditions[] = $dbr->orExpr( [ |
194 | 'translation_status' => $status, |
195 | 'cxsx_translation_status' => $statusIndex |
196 | ] ); |
197 | } |
198 | if ( $from !== null ) { |
199 | $whereConditions['translation_source_language'] = $from; |
200 | } |
201 | if ( $to !== null ) { |
202 | $whereConditions['translation_target_language'] = $to; |
203 | } |
204 | if ( $offset !== null ) { |
205 | $whereConditions[] = $dbr->expr( 'translation_last_updated_timestamp', '<', $dbr->timestamp( $offset ) ); |
206 | } |
207 | |
208 | return $dbr->newSelectQueryBuilder() |
209 | ->select( ISQLPlatform::ALL_ROWS ) |
210 | ->from( TranslationStore::TRANSLATION_TABLE_NAME ) |
211 | ->leftJoin( self::TABLE_NAME, null, 'translation_id = cxsx_translation_id' ) |
212 | ->where( $whereConditions ) |
213 | ->orderBy( 'translation_last_updated_timestamp', SelectQueryBuilder::SORT_DESC ) |
214 | ->limit( $limit ) |
215 | ->caller( __METHOD__ ) |
216 | ->fetchResultSet(); |
217 | } |
218 | |
219 | /** |
220 | * @param int $userId User ID |
221 | * @param string|null $from |
222 | * @param string|null $to |
223 | * @param int $limit How many results to return. Defaults to 100, same as for the "contenttranslation" list API |
224 | * @param string|null $offset Offset condition (timestamp) |
225 | * @return DraftTranslationDTO[] |
226 | */ |
227 | public function findDraftSectionTranslationsByUser( |
228 | int $userId, |
229 | ?string $from = null, |
230 | ?string $to = null, |
231 | int $limit = 100, |
232 | ?string $offset = null |
233 | ): array { |
234 | $resultSet = $this->doFindTranslationsByUser( |
235 | $userId, |
236 | $from, |
237 | $to, |
238 | self::TRANSLATION_STATUS_DRAFT, |
239 | $limit, |
240 | $offset |
241 | ); |
242 | |
243 | $result = []; |
244 | foreach ( $resultSet as $row ) { |
245 | $result[] = new DraftTranslationDTO( |
246 | $row->cxsx_id ? (int)$row->cxsx_id : null, |
247 | (int)$row->translation_id, |
248 | $row->cxsx_section_id ?? null, |
249 | $row->translation_source_title, |
250 | $row->translation_source_language, |
251 | $row->translation_target_language, |
252 | $row->translation_start_timestamp, |
253 | $row->translation_last_updated_timestamp, |
254 | self::TRANSLATION_STATUSES[ $row->cxsx_translation_status ] ?? $row->translation_status, |
255 | $row->translation_source_revision_id, |
256 | $row->cxsx_translation_progress ?? $row->translation_progress, |
257 | $row->translation_target_title, |
258 | $row->cxsx_source_section_title, |
259 | $row->cxsx_target_section_title, |
260 | ); |
261 | } |
262 | |
263 | return $result; |
264 | } |
265 | |
266 | /** |
267 | * @param int $userId User ID |
268 | * @param string|null $from |
269 | * @param string|null $to |
270 | * @param int $limit How many results to return. Defaults to 100, same as for the "contenttranslation" list API |
271 | * @param string|null $offset Offset condition (timestamp) |
272 | * @return PublishedTranslationDTO[] |
273 | */ |
274 | public function findPublishedSectionTranslationsByUser( |
275 | int $userId, |
276 | ?string $from = null, |
277 | ?string $to = null, |
278 | int $limit = 100, |
279 | ?string $offset = null |
280 | ): array { |
281 | $resultSet = $this->doFindTranslationsByUser( |
282 | $userId, |
283 | $from, |
284 | $to, |
285 | self::TRANSLATION_STATUS_PUBLISHED, |
286 | $limit, |
287 | $offset |
288 | ); |
289 | |
290 | /** |
291 | * An array of PublishedSectionTranslationDTO objects grouped by translation id |
292 | * @var PublishedSectionTranslationDTO[] $publishedSectionTranslations |
293 | */ |
294 | $publishedSectionTranslations = []; |
295 | $publishedStatusIndex = self::getStatusIndexByStatus( self::TRANSLATION_STATUS_PUBLISHED ); |
296 | |
297 | foreach ( $resultSet as $row ) { |
298 | $translationId = (int)$row->translation_id; |
299 | if ( $row->cxsx_id && $row->cxsx_translation_status === $publishedStatusIndex ) { |
300 | $publishedSectionTranslations[$translationId] = new PublishedSectionTranslationDTO( |
301 | (int)$row->cxsx_id, |
302 | $row->cxsx_section_id, |
303 | $row->cxsx_translation_start_timestamp, |
304 | $row->cxsx_translation_last_updated_timestamp, |
305 | $row->cxsx_source_section_title, |
306 | $row->cxsx_target_section_title, |
307 | ); |
308 | } |
309 | } |
310 | |
311 | $result = []; |
312 | foreach ( $resultSet as $row ) { |
313 | $translationId = (int)$row->translation_id; |
314 | // multiple rows can exist for the same translation id |
315 | // we only need one DTO per translation |
316 | if ( isset( $result[$translationId] ) ) { |
317 | continue; |
318 | } |
319 | $result[$translationId] = new PublishedTranslationDTO( |
320 | $translationId, |
321 | $row->translation_source_title, |
322 | $row->translation_source_language, |
323 | $row->translation_target_language, |
324 | $row->translation_start_timestamp, |
325 | $row->translation_last_updated_timestamp, |
326 | $row->translation_source_revision_id, |
327 | $row->translation_target_title, |
328 | $row->translation_target_url, |
329 | $publishedSectionTranslations[$translationId] ?? [] |
330 | ); |
331 | } |
332 | |
333 | return array_values( $result ); |
334 | } |
335 | |
336 | /** |
337 | * Given the "parent" translation id and the section id |
338 | * (in the "${revision}_${sectionNumber} form), this method |
339 | * deletes the corresponding section translation from the |
340 | * "cx_section_translations" table. |
341 | * |
342 | * @param int $sectionTranslationId |
343 | * @return void |
344 | */ |
345 | public function deleteTranslationById( int $sectionTranslationId ): void { |
346 | $dbw = $this->lb->getConnection( DB_PRIMARY ); |
347 | |
348 | $deletedStatusIndex = self::getStatusIndexByStatus( self::TRANSLATION_STATUS_DELETED ); |
349 | $dbw->newUpdateQueryBuilder() |
350 | ->update( self::TABLE_NAME ) |
351 | ->set( [ 'cxsx_translation_status' => $deletedStatusIndex ] ) |
352 | ->where( [ 'cxsx_id' => $sectionTranslationId ] ) |
353 | ->caller( __METHOD__ ) |
354 | ->execute(); |
355 | } |
356 | |
357 | private function translationToDBRow( SectionTranslation $translation ): array { |
358 | return [ |
359 | 'cxsx_translation_id' => $translation->getTranslationId(), |
360 | 'cxsx_section_id' => $translation->getSectionId(), |
361 | 'cxsx_source_section_title' => $translation->getSourceSectionTitle(), |
362 | 'cxsx_target_section_title' => $translation->getTargetSectionTitle(), |
363 | 'cxsx_translation_status' => $translation->getTranslationStatus(), |
364 | 'cxsx_translation_progress' => $translation->getProgress(), |
365 | ]; |
366 | } |
367 | } |