Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 177 |
|
0.00% |
0 / 13 |
CRAP | |
0.00% |
0 / 1 |
MessageGroupStatsSpecialPage | |
0.00% |
0 / 177 |
|
0.00% |
0 / 13 |
2550 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
getDescription | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isIncludable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 87 |
|
0.00% |
0 / 1 |
702 | |||
loadStatistics | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCacheRebuildJobParameters | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isValidGroup | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
invalidTarget | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
outputIntroduction | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
formatLanguageList | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
addForm | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
20 | |||
getGroupOptions | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\Statistics; |
5 | |
6 | use Config; |
7 | use DeferredUpdates; |
8 | use HTMLForm; |
9 | use JobQueueGroup; |
10 | use MediaWiki\Config\ServiceOptions; |
11 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups; |
12 | use MediaWiki\Extension\Translate\MessageProcessing\MessageGroupMetadata; |
13 | use MediaWiki\Extension\Translate\TranslatorInterface\EntitySearch; |
14 | use MediaWiki\Html\Html; |
15 | use MediaWiki\Languages\LanguageNameUtils; |
16 | use MessageGroupStats; |
17 | use MessagePrefixMessageGroup; |
18 | use SpecialPage; |
19 | |
20 | /** |
21 | * Implements includable special page Special:MessageGroupStats which provides |
22 | * translation statistics for all languages for a group. |
23 | * |
24 | * @author Niklas Laxström |
25 | * @author Siebrand Mazeland |
26 | * @license GPL-2.0-or-later |
27 | * @ingroup SpecialPage TranslateSpecialPage Stats |
28 | */ |
29 | class MessageGroupStatsSpecialPage extends SpecialPage { |
30 | /** Whether to hide rows which are fully translated. */ |
31 | private bool $noComplete = true; |
32 | /** Whether to hide rows which are fully untranslated. */ |
33 | private bool $noEmpty = false; |
34 | /** The target of stats: group id or message prefix. */ |
35 | private string $target; |
36 | /** The target type of stats requested: */ |
37 | private ?string $targetType = null; |
38 | private ServiceOptions $options; |
39 | private JobQueueGroup $jobQueueGroup; |
40 | private MessageGroupStatsTableFactory $messageGroupStatsTableFactory; |
41 | private EntitySearch $entitySearch; |
42 | private MessagePrefixStats $messagePrefixStats; |
43 | private LanguageNameUtils $languageNameUtils; |
44 | private MessageGroupMetadata $messageGroupMetadata; |
45 | |
46 | private const GROUPS = 'group'; |
47 | private const MESSAGES = 'messages'; |
48 | |
49 | private const CONSTRUCTOR_OPTIONS = [ |
50 | 'TranslateMessagePrefixStatsLimit', |
51 | ]; |
52 | |
53 | public function __construct( |
54 | Config $config, |
55 | JobQueueGroup $jobQueueGroup, |
56 | MessageGroupStatsTableFactory $messageGroupStatsTableFactory, |
57 | EntitySearch $entitySearch, |
58 | MessagePrefixStats $messagePrefixStats, |
59 | LanguageNameUtils $languageNameUtils, |
60 | MessageGroupMetadata $messageGroupMetadata |
61 | ) { |
62 | parent::__construct( 'MessageGroupStats' ); |
63 | $this->options = new ServiceOptions( self::CONSTRUCTOR_OPTIONS, $config ); |
64 | $this->jobQueueGroup = $jobQueueGroup; |
65 | $this->messageGroupStatsTableFactory = $messageGroupStatsTableFactory; |
66 | $this->entitySearch = $entitySearch; |
67 | $this->messagePrefixStats = $messagePrefixStats; |
68 | $this->languageNameUtils = $languageNameUtils; |
69 | $this->messageGroupMetadata = $messageGroupMetadata; |
70 | } |
71 | |
72 | public function getDescription() { |
73 | // Backward compatibility for < 1.41 |
74 | if ( version_compare( MW_VERSION, '1.41', '<' ) ) { |
75 | return $this->msg( 'translate-mgs-pagename' )->text(); |
76 | } |
77 | return $this->msg( 'translate-mgs-pagename' ); |
78 | } |
79 | |
80 | public function isIncludable() { |
81 | return true; |
82 | } |
83 | |
84 | protected function getGroupName() { |
85 | return 'translation'; |
86 | } |
87 | |
88 | public function execute( $par ) { |
89 | $request = $this->getRequest(); |
90 | |
91 | $purge = $request->getVal( 'action' ) === 'purge'; |
92 | if ( $purge && !$request->wasPosted() ) { |
93 | LanguageStatsSpecialPage::showPurgeForm( $this->getContext() ); |
94 | return; |
95 | } |
96 | |
97 | $this->setHeaders(); |
98 | $this->outputHeader(); |
99 | |
100 | $out = $this->getOutput(); |
101 | |
102 | $out->addModules( 'ext.translate.special.languagestats' ); |
103 | $out->addModuleStyles( 'ext.translate.statstable' ); |
104 | $out->addModuleStyles( 'ext.translate.special.groupstats' ); |
105 | |
106 | $params = $par ? explode( '/', $par ) : []; |
107 | |
108 | if ( isset( $params[0] ) && trim( $params[0] ) ) { |
109 | $this->target = $params[0]; |
110 | } |
111 | |
112 | if ( isset( $params[1] ) ) { |
113 | $this->noComplete = (bool)$params[1]; |
114 | } |
115 | |
116 | if ( isset( $params[2] ) ) { |
117 | $this->noEmpty = (bool)$params[2]; |
118 | } |
119 | |
120 | // Whether the form has been submitted, only relevant if not including |
121 | $submitted = !$this->including() && $request->getVal( 'x' ) === 'D'; |
122 | |
123 | // @phan-suppress-next-line PhanCoalescingNeverNull Need to check if the property is initialized |
124 | $this->target = $request->getVal( self::GROUPS, $this->target ?? '' ); |
125 | if ( $this->target !== '' ) { |
126 | $this->targetType = self::GROUPS; |
127 | } else { |
128 | $this->target = $request->getVal( self::MESSAGES, '' ); |
129 | if ( $this->target !== '' ) { |
130 | $this->targetType = self::MESSAGES; |
131 | } |
132 | } |
133 | |
134 | // Default booleans to false if the form was submitted |
135 | $this->noComplete = $request->getBool( |
136 | 'suppresscomplete', |
137 | $this->noComplete && !$submitted |
138 | ); |
139 | $this->noEmpty = $request->getBool( 'suppressempty', $this->noEmpty && !$submitted ); |
140 | |
141 | if ( !$this->including() ) { |
142 | $out->addHelpLink( 'Help:Extension:Translate/Statistics_and_reporting' ); |
143 | $this->addForm(); |
144 | } |
145 | |
146 | $stats = $output = null; |
147 | if ( $this->targetType === self::GROUPS && $this->isValidGroup( $this->target ) ) { |
148 | $this->outputIntroduction(); |
149 | |
150 | $stats = $this->loadStatistics( $this->target, MessageGroupStats::FLAG_CACHE_ONLY ); |
151 | |
152 | $messageGroupStatsTable = $this->messageGroupStatsTableFactory->newFromContext( $this->getContext() ); |
153 | $output = $messageGroupStatsTable->get( |
154 | $stats, |
155 | MessageGroups::getGroup( $this->target ), |
156 | $this->noComplete, |
157 | $this->noEmpty |
158 | ); |
159 | |
160 | $incomplete = $messageGroupStatsTable->areStatsIncomplete(); |
161 | if ( $incomplete ) { |
162 | $out->wrapWikiMsg( |
163 | "<div class='error'>$1</div>", |
164 | 'translate-langstats-incomplete' |
165 | ); |
166 | } |
167 | |
168 | if ( $incomplete || $purge ) { |
169 | DeferredUpdates::addCallableUpdate( function () use ( $purge ) { |
170 | // Attempt to recache on the fly the missing stats, unless a |
171 | // purge was requested, because that is likely to time out. |
172 | // Even though this is executed inside a deferred update, it |
173 | // counts towards the maximum execution time limit. If that is |
174 | // reached, or any other failure happens, no updates at all |
175 | // will be written into the database, as it does only single |
176 | // update at the end. Hence we always add a job too, so that |
177 | // even the slower updates will get done at some point. In |
178 | // regular case (no purge), the job sees that the stats are |
179 | // already updated, so it is not much of an overhead. |
180 | $jobParams = $this->getCacheRebuildJobParameters( $this->target ); |
181 | $jobParams[ 'purge' ] = $purge; |
182 | $job = RebuildMessageGroupStatsJob::newJob( $jobParams ); |
183 | $this->jobQueueGroup->push( $job ); |
184 | |
185 | // $purge is only true if request was posted |
186 | if ( !$purge ) { |
187 | $this->loadStatistics( $this->target ); |
188 | } |
189 | } ); |
190 | } |
191 | } elseif ( $this->targetType === self::MESSAGES ) { |
192 | $messagesWithPrefix = $this->entitySearch->matchMessages( $this->target ); |
193 | if ( $messagesWithPrefix ) { |
194 | $messageWithPrefixLimit = $this->options->get( 'TranslateMessagePrefixStatsLimit' ); |
195 | if ( count( $messagesWithPrefix ) > $messageWithPrefixLimit ) { |
196 | $out->addHTML( |
197 | Html::errorBox( |
198 | $this->msg( 'translate-mgs-message-prefix-limit' ) |
199 | ->params( $messageWithPrefixLimit ) |
200 | ->parse() |
201 | ) |
202 | ); |
203 | return; |
204 | } |
205 | |
206 | $stats = $this->messagePrefixStats->forAll( ...$messagesWithPrefix ); |
207 | $messageGroupStatsTable = $this->messageGroupStatsTableFactory |
208 | ->newFromContext( $this->getContext() ); |
209 | $output = $messageGroupStatsTable->get( |
210 | $stats, |
211 | new MessagePrefixMessageGroup(), |
212 | $this->noComplete, |
213 | $this->noEmpty |
214 | ); |
215 | } |
216 | } |
217 | |
218 | if ( $output ) { |
219 | // If output is present, put it on the page |
220 | $out->addHTML( $output ); |
221 | } elseif ( $stats !== null ) { |
222 | // Output not present, but stats are present. Probably an issue? |
223 | $out->addHTML( Html::warningBox( $this->msg( 'translate-mgs-nothing' )->parse() ) ); |
224 | } elseif ( $submitted ) { |
225 | $this->invalidTarget(); |
226 | } |
227 | } |
228 | |
229 | private function loadStatistics( string $target, int $flags = 0 ): array { |
230 | return MessageGroupStats::forGroup( $target, $flags ); |
231 | } |
232 | |
233 | private function getCacheRebuildJobParameters( string $target ): array { |
234 | return [ 'groupid' => $target ]; |
235 | } |
236 | |
237 | private function isValidGroup( ?string $value ): bool { |
238 | if ( $value === null ) { |
239 | return false; |
240 | } |
241 | |
242 | $group = MessageGroups::getGroup( $value ); |
243 | if ( $group ) { |
244 | if ( MessageGroups::isDynamic( $group ) ) { |
245 | /* Dynamic groups are not listed, but it is possible to end up |
246 | * on this page with a dynamic group by navigating from |
247 | * translation or proofreading activity or by giving group id |
248 | * of dynamic group explicitly. Ignore dynamic group to avoid |
249 | * throwing exceptions later. */ |
250 | $group = false; |
251 | } else { |
252 | $this->target = $group->getId(); |
253 | } |
254 | } |
255 | |
256 | return (bool)$group; |
257 | } |
258 | |
259 | private function invalidTarget(): void { |
260 | $this->getOutput()->wrapWikiMsg( |
261 | "<div class='error'>$1</div>", |
262 | [ 'translate-mgs-invalid-group', $this->target ] |
263 | ); |
264 | } |
265 | |
266 | private function outputIntroduction(): void { |
267 | $priorityLangs = $this->messageGroupMetadata->get( $this->target, 'prioritylangs' ); |
268 | if ( $priorityLangs ) { |
269 | $languagesFormatted = $this->formatLanguageList( explode( ',', $priorityLangs ) ); |
270 | $hasPriorityForce = $this->messageGroupMetadata->get( $this->target, 'priorityforce' ) === 'on'; |
271 | if ( $hasPriorityForce ) { |
272 | $this->getOutput()->addWikiMsg( 'tpt-priority-languages-force', $languagesFormatted ); |
273 | } else { |
274 | $this->getOutput()->addWikiMsg( 'tpt-priority-languages', $languagesFormatted ); |
275 | } |
276 | } |
277 | } |
278 | |
279 | private function formatLanguageList( array $codes ): string { |
280 | foreach ( $codes as &$value ) { |
281 | $value = $this->languageNameUtils->getLanguageName( $value, $this->getLanguage()->getCode() ) |
282 | . $this->msg( 'word-separator' )->plain() |
283 | . $this->msg( 'parentheses', $value )->plain(); |
284 | } |
285 | |
286 | return $this->getLanguage()->listToText( $codes ); |
287 | } |
288 | |
289 | private function addForm(): void { |
290 | $formDescriptor = [ |
291 | 'select' => [ |
292 | 'type' => 'select', |
293 | 'name' => self::GROUPS, |
294 | 'id' => self::GROUPS, |
295 | 'label' => $this->msg( 'translate-mgs-group' )->text(), |
296 | 'options' => $this->getGroupOptions(), |
297 | 'default' => $this->targetType === self::GROUPS ? $this->target : null, |
298 | 'cssclass' => 'message-group-selector' |
299 | ], |
300 | 'input' => [ |
301 | 'type' => 'text', |
302 | 'name' => self::MESSAGES, |
303 | 'id' => self::MESSAGES, |
304 | 'label' => $this->msg( 'translate-mgs-prefix' )->text(), |
305 | 'default' => $this->targetType === self::MESSAGES ? $this->target : null, |
306 | 'cssclass' => 'message-prefix-selector' |
307 | ], |
308 | 'nocomplete-check' => [ |
309 | 'type' => 'check', |
310 | 'name' => 'suppresscomplete', |
311 | 'id' => 'suppresscomplete', |
312 | 'label' => $this->msg( 'translate-mgs-nocomplete' )->text(), |
313 | 'default' => $this->noComplete, |
314 | ], |
315 | 'noempty-check' => [ |
316 | 'type' => 'check', |
317 | 'name' => 'suppressempty', |
318 | 'id' => 'suppressempty', |
319 | 'label' => $this->msg( 'translate-mgs-noempty' )->text(), |
320 | 'default' => $this->noEmpty, |
321 | ] |
322 | ]; |
323 | |
324 | $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() ); |
325 | |
326 | /* Since these pages are in the tabgroup with Special:Translate, |
327 | * it makes sense to retain the selected group/language parameter |
328 | * on post requests even when not relevant to the current page. */ |
329 | $val = $this->getRequest()->getVal( 'language' ); |
330 | if ( $val !== null ) { |
331 | $htmlForm->addHiddenField( 'language', $val ); |
332 | } |
333 | |
334 | $htmlForm |
335 | ->addHiddenField( 'x', 'D' ) // To detect submission |
336 | ->setMethod( 'get' ) |
337 | ->setId( 'mw-message-group-stats-form' ) |
338 | ->setSubmitTextMsg( 'translate-mgs-submit' ) |
339 | ->setWrapperLegendMsg( 'translate-mgs-fieldset' ) |
340 | ->prepareForm() |
341 | ->displayForm( false ); |
342 | } |
343 | |
344 | /** Creates a simple message group options. */ |
345 | private function getGroupOptions(): array { |
346 | $options = [ '' => null ]; |
347 | $groups = MessageGroups::getAllGroups(); |
348 | |
349 | foreach ( $groups as $id => $class ) { |
350 | if ( MessageGroups::getGroup( $id )->exists() ) { |
351 | $options[$class->getLabel()] = $id; |
352 | } |
353 | } |
354 | |
355 | return $options; |
356 | } |
357 | } |