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