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