Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 185 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryContentTranslation | |
0.00% |
0 / 185 |
|
0.00% |
0 / 10 |
992 | |
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 / 47 |
|
0.00% |
0 / 1 |
156 | |||
serveUnifiedDashboardTranslations | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
30 | |||
serveDesktopEditorDraft | |
0.00% |
0 / 41 |
|
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 | |
136 | private function serveUnifiedDashboardTranslations( array $params ): void { |
137 | $status = $params['type']; |
138 | |
139 | $sectionTranslations = []; |
140 | $user = $this->getUser(); |
141 | $translatorUserId = $this->userService->getGlobalUserId( $user ); |
142 | |
143 | if ( $status === SectionTranslationStore::TRANSLATION_STATUS_PUBLISHED ) { |
144 | $sectionTranslations = $this->sectionTranslationStore->findPublishedSectionTranslationsByUser( |
145 | $translatorUserId, |
146 | $params['from'], |
147 | $params['to'], |
148 | $params['limit'], |
149 | $params['offset'] |
150 | ); |
151 | } elseif ( $status === SectionTranslationStore::TRANSLATION_STATUS_DRAFT ) { |
152 | $sectionTranslations = $this->sectionTranslationStore->findDraftSectionTranslationsByUser( |
153 | $translatorUserId, |
154 | $params['from'], |
155 | $params['to'], |
156 | $params['limit'], |
157 | $params['offset'] |
158 | ); |
159 | } |
160 | |
161 | $translations = array_map( static function ( $sectionTranslation ) { |
162 | return $sectionTranslation->toArray(); |
163 | }, $sectionTranslations ); |
164 | |
165 | // We will have extra "continue" in case the last batch is exactly the size of the limit |
166 | $count = count( $sectionTranslations ); |
167 | |
168 | if ( $count === $params['limit'] ) { |
169 | $offset = $sectionTranslations[$count - 1]->getLastUpdatedTimestamp(); |
170 | // We will have extra "continue" in case the last batch is exactly the size of the limit |
171 | if ( $offset ) { |
172 | $this->setContinueEnumParameter( 'offset', $offset ); |
173 | } |
174 | } |
175 | |
176 | $result = $this->getResult(); |
177 | $result->addValue( [ 'query', 'contenttranslation' ], 'translations', $translations ); |
178 | } |
179 | |
180 | private function serveDesktopEditorDraft( array $params ): void { |
181 | $result = $this->getResult(); |
182 | [ 'sourcetitle' => $sourceTitle, 'from' => $sourceLanguage, 'to' => $targetLanguage ] = $params; |
183 | |
184 | $translation = $this->translationStore->findTranslationByUser( |
185 | $this->getUser(), |
186 | $sourceTitle, |
187 | $sourceLanguage, |
188 | $targetLanguage |
189 | ); |
190 | |
191 | if ( $translation instanceof Translation ) { |
192 | if ( $params['sourcesectiontitle'] ) { |
193 | $sectionTranslation = $this->sectionTranslationStore->findTranslationBySectionTitle( |
194 | $translation->getTranslationId(), |
195 | $params['sourcesectiontitle'] |
196 | ); |
197 | |
198 | $draftStatusIndex = SectionTranslationStore::getStatusIndexByStatus( |
199 | SectionTranslationStore::TRANSLATION_STATUS_DRAFT |
200 | ); |
201 | if ( $sectionTranslation?->getTranslationStatus() !== $draftStatusIndex ) { |
202 | return; |
203 | } |
204 | |
205 | $sectionTranslationId = $sectionTranslation?->getId(); |
206 | $targetSectionTitle = $sectionTranslation?->getTargetSectionTitle(); |
207 | $translation->translation['sectionTranslationId'] = $sectionTranslationId; |
208 | $translation->translation['targetSectionTitle'] = $targetSectionTitle; |
209 | } elseif ( $translation->getStatus() !== TranslationStore::TRANSLATION_STATUS_DRAFT ) { |
210 | return; |
211 | } |
212 | |
213 | $this->addUnitsAndCategoriesToTranslation( $translation ); |
214 | $draftDTO = CXDraftTranslationDTO::createFromTranslation( $translation ); |
215 | |
216 | $result->addValue( [ 'query', 'contenttranslation' ], 'translation', $draftDTO->toArray() ); |
217 | } else { |
218 | // Check for other drafts. If one exists, return that to the UI which will then |
219 | // know to display an error to the user because we disallow two users to start |
220 | // drafts on the same translation work. |
221 | $conflictingTranslations = $this->translationStore->findConflictingDraftTranslations( |
222 | $sourceTitle, |
223 | $sourceLanguage, |
224 | $targetLanguage |
225 | ); |
226 | |
227 | if ( !$conflictingTranslations ) { |
228 | return; |
229 | } |
230 | |
231 | // if at least one conflicting translation is found, let the UI know |
232 | $result->addValue( [ 'query', 'contenttranslation' ], 'hasConflicts', true ); |
233 | // Take only the last conflicting translation due to UI limitations |
234 | $translation = array_pop( $conflictingTranslations ); |
235 | // $globalUserId is always expected to be integer or null, since it has been populated |
236 | // by the "translation_started_by" column of "cx_translations" table |
237 | $globalUserId = $translation->getData()['lastUpdatedTranslator']; |
238 | // $user can be null if the local user does not exist. Currently, this should never happen |
239 | // in our case because we redirect translators to the target wiki, and they cannot |
240 | // do translations without logging in. |
241 | // $user can also be null, if the current user has no permission to see the username. |
242 | // For whatever reason, fallback gracefully by letting 'translatorName' and 'translatorGender' |
243 | // to be null. |
244 | [ 'name' => $name, 'gender' => $gender ] = $this->userService->getUsernameAndGender( $globalUserId ); |
245 | // Add name and gender information to the returned result. The UI can use this |
246 | // to display the conflict message. |
247 | $result->addValue( [ 'query', 'contenttranslation' ], 'translatorName', $name ); |
248 | $result->addValue( [ 'query', 'contenttranslation' ], 'translatorGender', $gender ); |
249 | } |
250 | } |
251 | |
252 | /** |
253 | * @param int $translationId |
254 | */ |
255 | private function serveTranslationCorporaUnits( $translationId ) { |
256 | $translation = $this->translationStore->findByUserAndId( $this->getUser(), $translationId ); |
257 | if ( $translation !== null ) { |
258 | $this->addUnitsAndCategoriesToTranslation( $translation ); |
259 | $result = $this->getResult(); |
260 | $result->addValue( [ 'query', 'contenttranslation' ], 'translation', $translation->translation ); |
261 | } else { |
262 | $this->dieWithError( 'apierror-cx-missingdraft', 'missingdraft' ); |
263 | } |
264 | } |
265 | |
266 | /** @inheritDoc */ |
267 | public function getAllowedParams() { |
268 | $allowedParams = [ |
269 | 'translationid' => [ |
270 | ParamValidator::PARAM_TYPE => 'string', |
271 | ], |
272 | 'from' => [ |
273 | ParamValidator::PARAM_TYPE => 'string', |
274 | ], |
275 | 'to' => [ |
276 | ParamValidator::PARAM_TYPE => 'string', |
277 | ], |
278 | 'sourcetitle' => [ |
279 | ParamValidator::PARAM_TYPE => 'string', |
280 | ], |
281 | 'sourcesectiontitle' => [ |
282 | ParamValidator::PARAM_TYPE => 'string', |
283 | ], |
284 | 'limit' => [ |
285 | ParamValidator::PARAM_DEFAULT => 100, |
286 | ParamValidator::PARAM_TYPE => 'limit', |
287 | IntegerDef::PARAM_MIN => 1, |
288 | IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1, |
289 | IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2 |
290 | ], |
291 | 'offset' => [ |
292 | ParamValidator::PARAM_DEFAULT => null, |
293 | ParamValidator::PARAM_TYPE => 'string', |
294 | ], |
295 | 'type' => [ |
296 | ParamValidator::PARAM_DEFAULT => 'draft', |
297 | ParamValidator::PARAM_TYPE => [ 'draft', 'published' ], |
298 | ], |
299 | 'usecase' => [ |
300 | ParamValidator::PARAM_DEFAULT => null, |
301 | ParamValidator::PARAM_TYPE => [ |
302 | 'unified-dashboard', |
303 | 'desktop-editor-draft', |
304 | 'translation-corpora-units' |
305 | ], |
306 | ] |
307 | ]; |
308 | return $allowedParams; |
309 | } |
310 | |
311 | /** @inheritDoc */ |
312 | private function addUnitsAndCategoriesToTranslation( Translation $translation ): void { |
313 | // Translation units and target categories. Only target categories are fetched |
314 | // when translation draft is restored. Source categories are saved into cx_corpora table for |
315 | // pairing with target categories, but not retrieved when translation draft is restored. |
316 | // Associative array with 'translationUnits' and 'categories' data |
317 | $unitsAndCategories = $this->corporaManager->getUnitsAndCategoriesByTranslationId( |
318 | (int)$translation->getTranslationId() |
319 | ); |
320 | $translation->translation['translationUnits'] = $unitsAndCategories['translationUnits']; |
321 | $translation->translation['targetCategories'] = $unitsAndCategories['categories']; |
322 | } |
323 | |
324 | /** @inheritDoc */ |
325 | protected function getExamplesMessages() { |
326 | return [ |
327 | 'action=query&list=contenttranslation' => |
328 | 'apihelp-query+contenttranslation-example-1', |
329 | 'action=query&list=contenttranslation&translationid=94' => |
330 | 'apihelp-query+contenttranslation-example-2', |
331 | 'action=query&list=contenttranslation&from=en&to=es&sourcetitle=Hibiscus' => |
332 | 'apihelp-query+contenttranslation-example-3', |
333 | ]; |
334 | } |
335 | } |