Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 191 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryContentTranslation | |
0.00% |
0 / 191 |
|
0.00% |
0 / 10 |
1260 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
executeGenerator | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
run | |
0.00% |
0 / 53 |
|
0.00% |
0 / 1 |
182 | |||
serveUnifiedDashboardTranslations | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
72 | |||
serveDesktopEditorDraft | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
42 | |||
serveTranslationCorporaUnits | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getAllowedParams | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
2 | |||
addUnitsAndCategoriesToTranslation | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getExamplesMessages | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Api module for querying Content translations. |
4 | * |
5 | * @copyright See AUTHORS.txt |
6 | * @license GPL-2.0-or-later |
7 | */ |
8 | |
9 | namespace ContentTranslation\ActionApi; |
10 | |
11 | use ContentTranslation\DTO\CXDraftTranslationDTO; |
12 | use ContentTranslation\Manager\TranslationCorporaManager; |
13 | use ContentTranslation\Service\UserService; |
14 | use ContentTranslation\Store\SectionTranslationStore; |
15 | use ContentTranslation\Store\TranslationStore; |
16 | use ContentTranslation\Translation; |
17 | use ContentTranslation\Translator; |
18 | use MediaWiki\Api\ApiBase; |
19 | use MediaWiki\Api\ApiPageSet; |
20 | use MediaWiki\Api\ApiQuery; |
21 | use MediaWiki\Api\ApiQueryGeneratorBase; |
22 | use Wikimedia\ParamValidator\ParamValidator; |
23 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
24 | |
25 | /** |
26 | * Api module for querying ContentTranslation. |
27 | */ |
28 | class ApiQueryContentTranslation extends ApiQueryGeneratorBase { |
29 | |
30 | private SectionTranslationStore $sectionTranslationStore; |
31 | private TranslationCorporaManager $corporaManager; |
32 | private UserService $userService; |
33 | private TranslationStore $translationStore; |
34 | |
35 | public function __construct( |
36 | ApiQuery $query, |
37 | string $moduleName, |
38 | SectionTranslationStore $sectionTranslationStore, |
39 | TranslationCorporaManager $corporaManager, |
40 | UserService $userService, |
41 | TranslationStore $translationStore |
42 | ) { |
43 | parent::__construct( $query, $moduleName ); |
44 | |
45 | $this->sectionTranslationStore = $sectionTranslationStore; |
46 | $this->corporaManager = $corporaManager; |
47 | $this->userService = $userService; |
48 | $this->translationStore = $translationStore; |
49 | } |
50 | |
51 | public function execute() { |
52 | $this->run(); |
53 | } |
54 | |
55 | /** @inheritDoc */ |
56 | public function executeGenerator( $resultPageSet ) { |
57 | $this->run( $resultPageSet ); |
58 | } |
59 | |
60 | /** |
61 | * @param ApiPageSet|null $resultPageSet |
62 | */ |
63 | private function run( $resultPageSet = null ) { |
64 | $params = $this->extractRequestParams(); |
65 | $result = $this->getResult(); |
66 | $user = $this->getUser(); |
67 | [ 'sourcetitle' => $sourceTitle, 'from' => $sourceLanguage, 'to' => $targetLanguage ] = $params; |
68 | |
69 | // Case A: Find a translation for given work from anonymous context |
70 | if ( !$user->isRegistered() ) { |
71 | if ( $params['translationid'] ) { |
72 | $this->dieWithError( 'apierror-cx-mustbeloggedin-viewtranslations', 'notloggedin' ); |
73 | } |
74 | if ( $sourceTitle && $sourceLanguage && $targetLanguage ) { |
75 | $translation = $this->translationStore->findTranslationByTitle( |
76 | $sourceTitle, |
77 | $sourceLanguage, |
78 | $targetLanguage |
79 | ); |
80 | |
81 | if ( $translation === null ) { |
82 | $this->dieWithError( 'apierror-cx-translationnotfound', 'translationnotfound' ); |
83 | } |
84 | |
85 | $result->addValue( |
86 | [ 'query', 'contenttranslation' ], |
87 | 'translation', |
88 | $translation->translation |
89 | ); |
90 | } |
91 | |
92 | return; |
93 | } |
94 | |
95 | if ( $params['usecase'] === 'unified-dashboard' ) { |
96 | $this->serveUnifiedDashboardTranslations( $params ); |
97 | |
98 | return; |
99 | } elseif ( $params['usecase'] === 'desktop-editor-draft' ) { |
100 | $this->serveDesktopEditorDraft( $params ); |
101 | |
102 | return; |
103 | } elseif ( $params['usecase'] === 'translation-corpora-units' ) { |
104 | $this->serveTranslationCorporaUnits( $params['translationid'] ); |
105 | |
106 | return; |
107 | } |
108 | |
109 | $translator = new Translator( $user ); |
110 | |
111 | // Case D: Find list of translations. Either section translations or article translations |
112 | $offset = null; |
113 | $translatorUserId = $this->userService->getGlobalUserId( $user ); |
114 | $translations = $this->translationStore->getAllTranslationsByUserId( |
115 | $translatorUserId, |
116 | $params['limit'], |
117 | $params['offset'], |
118 | $params['type'], |
119 | $sourceLanguage, |
120 | $targetLanguage |
121 | ); |
122 | |
123 | $count = count( $translations ); |
124 | if ( $count === $params['limit'] ) { |
125 | $offset = $translations[$count - 1]->translation['lastUpdateTimestamp']; |
126 | } |
127 | |
128 | // We will have extra "continue" in case the last batch is exactly the size of the limit |
129 | if ( $offset ) { |
130 | $this->setContinueEnumParameter( 'offset', $offset ); |
131 | } |
132 | |
133 | $result->addValue( [ 'query', 'contenttranslation' ], 'translations', $translations ); |
134 | |
135 | // Simple optimization |
136 | if ( $params['offset'] === null ) { |
137 | $result->addValue( |
138 | [ 'query', 'contenttranslation' ], |
139 | 'languages', |
140 | $translator->getLanguages( $params['type'] ) |
141 | ); |
142 | } |
143 | } |
144 | |
145 | private function serveUnifiedDashboardTranslations( array $params ): void { |
146 | $status = $params['type']; |
147 | |
148 | if ( !$status || !in_array( $status, SectionTranslationStore::TRANSLATION_STATUSES ) ) { |
149 | $this->dieWithError( 'apierror-cx-invalid-type-viewtranslations', 'invalidtype' ); |
150 | } |
151 | |
152 | $sectionTranslations = []; |
153 | $user = $this->getUser(); |
154 | $translatorUserId = $this->userService->getGlobalUserId( $user ); |
155 | |
156 | if ( $status === SectionTranslationStore::TRANSLATION_STATUS_PUBLISHED ) { |
157 | $sectionTranslations = $this->sectionTranslationStore->findPublishedSectionTranslationsByUser( |
158 | $translatorUserId, |
159 | $params['from'], |
160 | $params['to'], |
161 | $params['limit'], |
162 | $params['offset'] |
163 | ); |
164 | } elseif ( $status === SectionTranslationStore::TRANSLATION_STATUS_DRAFT ) { |
165 | $sectionTranslations = $this->sectionTranslationStore->findDraftSectionTranslationsByUser( |
166 | $translatorUserId, |
167 | $params['from'], |
168 | $params['to'], |
169 | $params['limit'], |
170 | $params['offset'] |
171 | ); |
172 | } |
173 | |
174 | $translations = array_map( static function ( $sectionTranslation ) { |
175 | return $sectionTranslation->toArray(); |
176 | }, $sectionTranslations ); |
177 | |
178 | // We will have extra "continue" in case the last batch is exactly the size of the limit |
179 | $count = count( $sectionTranslations ); |
180 | |
181 | if ( $count === $params['limit'] ) { |
182 | $offset = $sectionTranslations[$count - 1]->getLastUpdatedTimestamp(); |
183 | // We will have extra "continue" in case the last batch is exactly the size of the limit |
184 | if ( $offset ) { |
185 | $this->setContinueEnumParameter( 'offset', $offset ); |
186 | } |
187 | } |
188 | |
189 | $result = $this->getResult(); |
190 | $result->addValue( [ 'query', 'contenttranslation' ], 'translations', $translations ); |
191 | |
192 | // Simple optimization |
193 | if ( $params['offset'] === null ) { |
194 | $translator = new Translator( $user ); |
195 | $translatorLanguages = $translator->getLanguages( $params['type'] ); |
196 | $result->addValue( [ 'query', 'contenttranslation' ], 'languages', $translatorLanguages ); |
197 | } |
198 | } |
199 | |
200 | private function serveDesktopEditorDraft( array $params ): void { |
201 | $result = $this->getResult(); |
202 | [ 'sourcetitle' => $sourceTitle, 'from' => $sourceLanguage, 'to' => $targetLanguage ] = $params; |
203 | |
204 | $translation = $this->translationStore->findTranslationByUser( |
205 | $this->getUser(), |
206 | $sourceTitle, |
207 | $sourceLanguage, |
208 | $targetLanguage, |
209 | TranslationStore::TRANSLATION_STATUS_DRAFT |
210 | ); |
211 | |
212 | if ( $translation instanceof Translation ) { |
213 | if ( $params['sourcesectiontitle'] ) { |
214 | $sectionTranslation = $this->sectionTranslationStore->findTranslationBySectionTitle( |
215 | $translation->getTranslationId(), |
216 | $params['sourcesectiontitle'] |
217 | ); |
218 | |
219 | $sectionTranslationId = $sectionTranslation ? $sectionTranslation->getId() : null; |
220 | $targetSectionTitle = $sectionTranslation ? $sectionTranslation->getTargetSectionTitle() : null; |
221 | $translation->translation['sectionTranslationId'] = $sectionTranslationId; |
222 | $translation->translation['targetSectionTitle'] = $targetSectionTitle; |
223 | } |
224 | |
225 | $this->addUnitsAndCategoriesToTranslation( $translation ); |
226 | $draftDTO = CXDraftTranslationDTO::createFromTranslation( $translation ); |
227 | |
228 | $result->addValue( [ 'query', 'contenttranslation' ], 'translation', $draftDTO->toArray() ); |
229 | } else { |
230 | // Check for other drafts. If one exists, return that to the UI which will then |
231 | // know to display an error to the user because we disallow two users to start |
232 | // drafts on the same translation work. |
233 | $conflictingTranslations = $this->translationStore->findConflictingDraftTranslations( |
234 | $sourceTitle, |
235 | $sourceLanguage, |
236 | $targetLanguage |
237 | ); |
238 | |
239 | if ( !$conflictingTranslations ) { |
240 | return; |
241 | } |
242 | |
243 | // if at least one conflicting translation is found, let the UI know |
244 | $result->addValue( [ 'query', 'contenttranslation' ], 'hasConflicts', true ); |
245 | // Take only the last conflicting translation due to UI limitations |
246 | $translation = array_pop( $conflictingTranslations ); |
247 | // $globalUserId is always expected to be integer or null, since it has been populated |
248 | // by the "translation_started_by" column of "cx_translations" table |
249 | $globalUserId = $translation->getData()['lastUpdatedTranslator']; |
250 | // $user can be null if the local user does not exist. Currently, this should never happen |
251 | // in our case because we redirect translators to the target wiki, and they cannot |
252 | // do translations without logging in. |
253 | // $user can also be null, if the current user has no permission to see the username. |
254 | // For whatever reason, fallback gracefully by letting 'translatorName' and 'translatorGender' |
255 | // to be null. |
256 | [ 'name' => $name, 'gender' => $gender ] = $this->userService->getUsernameAndGender( $globalUserId ); |
257 | // Add name and gender information to the returned result. The UI can use this |
258 | // to display the conflict message. |
259 | $result->addValue( [ 'query', 'contenttranslation' ], 'translatorName', $name ); |
260 | $result->addValue( [ 'query', 'contenttranslation' ], 'translatorGender', $gender ); |
261 | } |
262 | } |
263 | |
264 | /** |
265 | * @param int $translationId |
266 | */ |
267 | private function serveTranslationCorporaUnits( $translationId ) { |
268 | $translation = $this->translationStore->findByUserAndId( $this->getUser(), $translationId ); |
269 | if ( $translation !== null ) { |
270 | $this->addUnitsAndCategoriesToTranslation( $translation ); |
271 | $result = $this->getResult(); |
272 | $result->addValue( [ 'query', 'contenttranslation' ], 'translation', $translation->translation ); |
273 | } else { |
274 | $this->dieWithError( 'apierror-cx-missingdraft', 'missingdraft' ); |
275 | } |
276 | } |
277 | |
278 | /** @inheritDoc */ |
279 | public function getAllowedParams() { |
280 | $allowedParams = [ |
281 | 'translationid' => [ |
282 | ParamValidator::PARAM_TYPE => 'string', |
283 | ], |
284 | 'from' => [ |
285 | ParamValidator::PARAM_TYPE => 'string', |
286 | ], |
287 | 'to' => [ |
288 | ParamValidator::PARAM_TYPE => 'string', |
289 | ], |
290 | 'sourcetitle' => [ |
291 | ParamValidator::PARAM_TYPE => 'string', |
292 | ], |
293 | 'sourcesectiontitle' => [ |
294 | ParamValidator::PARAM_TYPE => 'string', |
295 | ], |
296 | 'limit' => [ |
297 | ParamValidator::PARAM_DEFAULT => 100, |
298 | ParamValidator::PARAM_TYPE => 'limit', |
299 | IntegerDef::PARAM_MIN => 1, |
300 | IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1, |
301 | IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2 |
302 | ], |
303 | 'offset' => [ |
304 | ParamValidator::PARAM_DEFAULT => null, |
305 | ParamValidator::PARAM_TYPE => 'string', |
306 | ], |
307 | 'type' => [ |
308 | ParamValidator::PARAM_DEFAULT => null, |
309 | ParamValidator::PARAM_TYPE => [ 'draft', 'published' ], |
310 | ], |
311 | 'usecase' => [ |
312 | ParamValidator::PARAM_DEFAULT => null, |
313 | ParamValidator::PARAM_TYPE => [ |
314 | 'unified-dashboard', |
315 | 'desktop-editor-draft', |
316 | 'translation-corpora-units' |
317 | ], |
318 | ] |
319 | ]; |
320 | return $allowedParams; |
321 | } |
322 | |
323 | /** @inheritDoc */ |
324 | private function addUnitsAndCategoriesToTranslation( Translation $translation ): void { |
325 | // Translation units and target categories. Only target categories are fetched |
326 | // when translation draft is restored. Source categories are saved into cx_corpora table for |
327 | // pairing with target categories, but not retrieved when translation draft is restored. |
328 | // Associative array with 'translationUnits' and 'categories' data |
329 | $unitsAndCategories = $this->corporaManager->getUnitsAndCategoriesByTranslationId( |
330 | (int)$translation->getTranslationId() |
331 | ); |
332 | $translation->translation['translationUnits'] = $unitsAndCategories['translationUnits']; |
333 | $translation->translation['targetCategories'] = $unitsAndCategories['categories']; |
334 | } |
335 | |
336 | /** @inheritDoc */ |
337 | protected function getExamplesMessages() { |
338 | return [ |
339 | 'action=query&list=contenttranslation' => |
340 | 'apihelp-query+contenttranslation-example-1', |
341 | 'action=query&list=contenttranslation&translationid=94' => |
342 | 'apihelp-query+contenttranslation-example-2', |
343 | 'action=query&list=contenttranslation&from=en&to=es&sourcetitle=Hibiscus' => |
344 | 'apihelp-query+contenttranslation-example-3', |
345 | ]; |
346 | } |
347 | } |