Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
90.50% |
219 / 242 |
|
68.75% |
11 / 16 |
CRAP | |
0.00% |
0 / 1 |
SpecialConstraintReport | |
90.50% |
219 / 242 |
|
68.75% |
11 / 16 |
31.83 | |
0.00% |
0 / 1 |
factory | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
__construct | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
1 | |||
getModules | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDescription | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
82.05% |
32 / 39 |
|
0.00% |
0 / 1 |
7.28 | |||
buildEntityIdForm | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
1.00 | |||
buildNotice | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
getExplanationText | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
buildResultTable | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
2 | |||
appendToResultTable | |
94.59% |
35 / 37 |
|
0.00% |
0 / 1 |
3.00 | |||
buildResultHeader | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
buildSummary | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
formatStatus | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
2 | |||
getClaimLink | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
getClaimUrl | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | declare( strict_types = 1 ); |
4 | |
5 | namespace WikibaseQuality\ConstraintReport\Specials; |
6 | |
7 | use HtmlArmor; |
8 | use HTMLForm; |
9 | use IBufferingStatsdDataFactory; |
10 | use InvalidArgumentException; |
11 | use MediaWiki\Config\Config; |
12 | use MediaWiki\Html\Html; |
13 | use MediaWiki\SpecialPage\SpecialPage; |
14 | use OOUI\IconWidget; |
15 | use OOUI\LabelWidget; |
16 | use UnexpectedValueException; |
17 | use Wikibase\DataModel\Entity\EntityId; |
18 | use Wikibase\DataModel\Entity\EntityIdParser; |
19 | use Wikibase\DataModel\Entity\EntityIdParsingException; |
20 | use Wikibase\DataModel\Entity\ItemId; |
21 | use Wikibase\DataModel\Entity\NumericPropertyId; |
22 | use Wikibase\DataModel\Services\EntityId\EntityIdFormatter; |
23 | use Wikibase\DataModel\Services\Lookup\EntityLookup; |
24 | use Wikibase\Lib\LanguageFallbackChainFactory; |
25 | use Wikibase\Lib\Store\EntityTitleLookup; |
26 | use Wikibase\Repo\EntityIdLabelFormatterFactory; |
27 | use Wikibase\View\EntityIdFormatterFactory; |
28 | use WikibaseQuality\ConstraintReport\ConstraintCheck\DelegatingConstraintChecker; |
29 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessageRenderer; |
30 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessageRendererFactory; |
31 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult; |
32 | use WikibaseQuality\ConstraintReport\Html\HtmlTableBuilder; |
33 | use WikibaseQuality\ConstraintReport\Html\HtmlTableCellBuilder; |
34 | use WikibaseQuality\ConstraintReport\Html\HtmlTableHeaderBuilder; |
35 | |
36 | /** |
37 | * Special page that displays all constraints that are defined on an Entity with additional information |
38 | * (whether it complied or was a violation, which parameters the constraint has etc.). |
39 | * |
40 | * @author BP2014N1 |
41 | * @license GPL-2.0-or-later |
42 | */ |
43 | class SpecialConstraintReport extends SpecialPage { |
44 | |
45 | private EntityIdParser $entityIdParser; |
46 | private EntityLookup $entityLookup; |
47 | private EntityTitleLookup $entityTitleLookup; |
48 | private EntityIdFormatter $entityIdLabelFormatter; |
49 | private EntityIdFormatter $entityIdLinkFormatter; |
50 | private DelegatingConstraintChecker $constraintChecker; |
51 | private ViolationMessageRenderer $violationMessageRenderer; |
52 | private Config $config; |
53 | private IBufferingStatsdDataFactory $dataFactory; |
54 | |
55 | public static function factory( |
56 | Config $config, |
57 | IBufferingStatsdDataFactory $dataFactory, |
58 | EntityIdFormatterFactory $entityIdHtmlLinkFormatterFactory, |
59 | EntityIdLabelFormatterFactory $entityIdLabelFormatterFactory, |
60 | EntityIdParser $entityIdParser, |
61 | EntityTitleLookup $entityTitleLookup, |
62 | LanguageFallbackChainFactory $languageFallbackChainFactory, |
63 | EntityLookup $entityLookup, |
64 | DelegatingConstraintChecker $delegatingConstraintChecker, |
65 | ViolationMessageRendererFactory $violationMessageRendererFactory |
66 | ): self { |
67 | return new self( |
68 | $entityLookup, |
69 | $entityTitleLookup, |
70 | $entityIdLabelFormatterFactory, |
71 | $entityIdHtmlLinkFormatterFactory, |
72 | $entityIdParser, |
73 | $languageFallbackChainFactory, |
74 | $delegatingConstraintChecker, |
75 | $violationMessageRendererFactory, |
76 | $config, |
77 | $dataFactory |
78 | ); |
79 | } |
80 | |
81 | public function __construct( |
82 | EntityLookup $entityLookup, |
83 | EntityTitleLookup $entityTitleLookup, |
84 | EntityIdLabelFormatterFactory $entityIdLabelFormatterFactory, |
85 | EntityIdFormatterFactory $entityIdHtmlLinkFormatterFactory, |
86 | EntityIdParser $entityIdParser, |
87 | LanguageFallbackChainFactory $languageFallbackChainFactory, |
88 | DelegatingConstraintChecker $constraintChecker, |
89 | ViolationMessageRendererFactory $violationMessageRendererFactory, |
90 | Config $config, |
91 | IBufferingStatsdDataFactory $dataFactory |
92 | ) { |
93 | parent::__construct( 'ConstraintReport' ); |
94 | |
95 | $this->entityLookup = $entityLookup; |
96 | $this->entityTitleLookup = $entityTitleLookup; |
97 | $this->entityIdParser = $entityIdParser; |
98 | |
99 | $language = $this->getLanguage(); |
100 | |
101 | $this->entityIdLabelFormatter = $entityIdLabelFormatterFactory->getEntityIdFormatter( |
102 | $language |
103 | ); |
104 | |
105 | $this->entityIdLinkFormatter = $entityIdHtmlLinkFormatterFactory->getEntityIdFormatter( |
106 | $language |
107 | ); |
108 | |
109 | $this->constraintChecker = $constraintChecker; |
110 | |
111 | $this->violationMessageRenderer = $violationMessageRendererFactory->getViolationMessageRenderer( |
112 | $language, |
113 | $languageFallbackChainFactory->newFromLanguage( $language ), |
114 | $this->getContext() |
115 | ); |
116 | |
117 | $this->config = $config; |
118 | $this->dataFactory = $dataFactory; |
119 | } |
120 | |
121 | /** |
122 | * Returns array of modules that should be added |
123 | * |
124 | * @return string[] |
125 | */ |
126 | private function getModules(): array { |
127 | return [ |
128 | 'SpecialConstraintReportPage', |
129 | 'wikibase.quality.constraints.icon', |
130 | 'wikibase.alltargets', |
131 | ]; |
132 | } |
133 | |
134 | /** |
135 | * @see SpecialPage::getGroupName |
136 | * |
137 | * @return string |
138 | */ |
139 | protected function getGroupName() { |
140 | return 'wikibase'; |
141 | } |
142 | |
143 | /** |
144 | * @inheritDoc |
145 | */ |
146 | public function getDescription() { |
147 | return $this->msg( 'wbqc-constraintreport' ); |
148 | } |
149 | |
150 | /** |
151 | * @see SpecialPage::execute |
152 | * |
153 | * @param string|null $subPage |
154 | * |
155 | * @throws InvalidArgumentException |
156 | * @throws EntityIdParsingException |
157 | * @throws UnexpectedValueException |
158 | */ |
159 | public function execute( $subPage ) { |
160 | $out = $this->getOutput(); |
161 | |
162 | $postRequest = $this->getContext()->getRequest()->getVal( 'entityid' ); |
163 | if ( $postRequest ) { |
164 | $out->redirect( $this->getPageTitle( strtoupper( $postRequest ) )->getLocalURL() ); |
165 | return; |
166 | } |
167 | |
168 | $out->enableOOUI(); |
169 | $out->addModules( $this->getModules() ); |
170 | |
171 | $this->setHeaders(); |
172 | |
173 | $out->addHTML( $this->getExplanationText() ); |
174 | $this->buildEntityIdForm(); |
175 | |
176 | if ( !$subPage ) { |
177 | return; |
178 | } |
179 | |
180 | if ( !is_string( $subPage ) ) { |
181 | throw new InvalidArgumentException( '$subPage must be string.' ); |
182 | } |
183 | |
184 | try { |
185 | $entityId = $this->entityIdParser->parse( $subPage ); |
186 | } catch ( EntityIdParsingException $e ) { |
187 | $out->addHTML( |
188 | $this->buildNotice( 'wbqc-constraintreport-invalid-entity-id', true ) |
189 | ); |
190 | return; |
191 | } |
192 | |
193 | if ( !$this->entityLookup->hasEntity( $entityId ) ) { |
194 | $out->addHTML( |
195 | $this->buildNotice( 'wbqc-constraintreport-not-existent-entity', true ) |
196 | ); |
197 | return; |
198 | } |
199 | |
200 | $this->dataFactory->increment( |
201 | 'wikibase.quality.constraints.specials.specialConstraintReport.executeCheck' |
202 | ); |
203 | $results = $this->constraintChecker->checkAgainstConstraintsOnEntityId( $entityId ); |
204 | |
205 | if ( $results !== [] ) { |
206 | $out->addHTML( |
207 | $this->buildResultHeader( $entityId ) |
208 | . $this->buildSummary( $results ) |
209 | . $this->buildResultTable( $entityId, $results ) |
210 | ); |
211 | } else { |
212 | $out->addHTML( |
213 | $this->buildResultHeader( $entityId ) |
214 | . $this->buildNotice( 'wbqc-constraintreport-empty-result' ) |
215 | ); |
216 | } |
217 | } |
218 | |
219 | /** |
220 | * Builds html form for entity id input |
221 | */ |
222 | private function buildEntityIdForm(): void { |
223 | $formDescriptor = [ |
224 | 'entityid' => [ |
225 | 'class' => 'HTMLTextField', |
226 | 'section' => 'section', |
227 | 'name' => 'entityid', |
228 | 'label-message' => 'wbqc-constraintreport-form-entityid-label', |
229 | 'cssclass' => 'wbqc-constraintreport-form-entity-id', |
230 | 'placeholder' => $this->msg( 'wbqc-constraintreport-form-entityid-placeholder' )->escaped(), |
231 | ], |
232 | ]; |
233 | $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext(), 'wbqc-constraintreport-form' ); |
234 | $htmlForm->setSubmitText( $this->msg( 'wbqc-constraintreport-form-submit-label' )->escaped() ); |
235 | $htmlForm->setSubmitCallback( static function () { |
236 | return false; |
237 | } ); |
238 | $htmlForm->setMethod( 'post' ); |
239 | $htmlForm->show(); |
240 | } |
241 | |
242 | /** |
243 | * Builds notice with given message. Optionally notice can be handles as error by settings $error to true |
244 | * |
245 | * @param string $messageKey |
246 | * @param bool $error |
247 | * |
248 | * @throws InvalidArgumentException |
249 | * |
250 | * @return string HTML |
251 | */ |
252 | private function buildNotice( string $messageKey, bool $error = false ): string { |
253 | $cssClasses = 'wbqc-constraintreport-notice'; |
254 | if ( $error ) { |
255 | $cssClasses .= ' wbqc-constraintreport-notice-error'; |
256 | } |
257 | |
258 | return Html::rawElement( |
259 | 'p', |
260 | [ |
261 | 'class' => $cssClasses, |
262 | ], |
263 | $this->msg( $messageKey )->escaped() |
264 | ); |
265 | } |
266 | |
267 | /** |
268 | * @return string HTML |
269 | */ |
270 | private function getExplanationText(): string { |
271 | return Html::rawElement( |
272 | 'div', |
273 | [ 'class' => 'wbqc-explanation' ], |
274 | Html::rawElement( |
275 | 'p', |
276 | [], |
277 | $this->msg( 'wbqc-constraintreport-explanation-part-one' )->escaped() |
278 | ) |
279 | . Html::rawElement( |
280 | 'p', |
281 | [], |
282 | $this->msg( 'wbqc-constraintreport-explanation-part-two' )->escaped() |
283 | ) |
284 | ); |
285 | } |
286 | |
287 | /** |
288 | * @param EntityId $entityId |
289 | * @param CheckResult[] $results |
290 | * |
291 | * @return string HTML |
292 | */ |
293 | private function buildResultTable( EntityId $entityId, array $results ): string { |
294 | // Set table headers |
295 | $table = new HtmlTableBuilder( |
296 | [ |
297 | new HtmlTableHeaderBuilder( |
298 | $this->msg( 'wbqc-constraintreport-result-table-header-status' )->text(), |
299 | true |
300 | ), |
301 | new HtmlTableHeaderBuilder( |
302 | $this->msg( 'wbqc-constraintreport-result-table-header-property' )->text(), |
303 | true |
304 | ), |
305 | new HtmlTableHeaderBuilder( |
306 | $this->msg( 'wbqc-constraintreport-result-table-header-message' )->text(), |
307 | true |
308 | ), |
309 | new HtmlTableHeaderBuilder( |
310 | $this->msg( 'wbqc-constraintreport-result-table-header-constraint' )->text(), |
311 | true |
312 | ), |
313 | ] |
314 | ); |
315 | |
316 | foreach ( $results as $result ) { |
317 | $table = $this->appendToResultTable( $table, $entityId, $result ); |
318 | } |
319 | |
320 | return $table->toHtml(); |
321 | } |
322 | |
323 | private function appendToResultTable( |
324 | HtmlTableBuilder $table, |
325 | EntityId $entityId, |
326 | CheckResult $result |
327 | ): HtmlTableBuilder { |
328 | $message = $result->getMessage(); |
329 | if ( $message === null ) { |
330 | // no row for this result |
331 | return $table; |
332 | } |
333 | |
334 | // Status column |
335 | $statusColumn = $this->formatStatus( $result->getStatus() ); |
336 | |
337 | // Property column |
338 | $propertyId = new NumericPropertyId( $result->getContextCursor()->getSnakPropertyId() ); |
339 | $propertyColumn = $this->getClaimLink( |
340 | $entityId, |
341 | $propertyId, |
342 | $this->entityIdLabelFormatter->formatEntityId( $propertyId ) |
343 | ); |
344 | |
345 | // Message column |
346 | $messageColumn = $this->violationMessageRenderer->render( $message ); |
347 | |
348 | // Constraint column |
349 | $constraintTypeItemId = $result->getConstraint()->getConstraintTypeItemId(); |
350 | try { |
351 | $constraintTypeLabel = $this->entityIdLabelFormatter->formatEntityId( new ItemId( $constraintTypeItemId ) ); |
352 | } catch ( InvalidArgumentException $e ) { |
353 | $constraintTypeLabel = htmlspecialchars( $constraintTypeItemId ); |
354 | } |
355 | $constraintColumn = $this->getClaimLink( |
356 | $propertyId, |
357 | new NumericPropertyId( $this->config->get( 'WBQualityConstraintsPropertyConstraintId' ) ), |
358 | $constraintTypeLabel |
359 | ); |
360 | |
361 | // Append cells |
362 | $table->appendRow( |
363 | [ |
364 | new HtmlTableCellBuilder( |
365 | new HtmlArmor( $statusColumn ) |
366 | ), |
367 | new HtmlTableCellBuilder( |
368 | new HtmlArmor( $propertyColumn ) |
369 | ), |
370 | new HtmlTableCellBuilder( |
371 | new HtmlArmor( $messageColumn ) |
372 | ), |
373 | new HtmlTableCellBuilder( |
374 | new HtmlArmor( $constraintColumn ) |
375 | ), |
376 | ] |
377 | ); |
378 | |
379 | return $table; |
380 | } |
381 | |
382 | /** |
383 | * Returns html text of the result header |
384 | * |
385 | * @param EntityId $entityId |
386 | * |
387 | * @return string HTML |
388 | */ |
389 | protected function buildResultHeader( EntityId $entityId ): string { |
390 | $entityLink = sprintf( '%s (%s)', |
391 | $this->entityIdLinkFormatter->formatEntityId( $entityId ), |
392 | htmlspecialchars( $entityId->getSerialization() ) ); |
393 | |
394 | return Html::rawElement( |
395 | 'h3', |
396 | [], |
397 | sprintf( '%s %s', $this->msg( 'wbqc-constraintreport-result-headline' )->escaped(), $entityLink ) |
398 | ); |
399 | } |
400 | |
401 | /** |
402 | * Builds summary from given results |
403 | * |
404 | * @param CheckResult[] $results |
405 | * |
406 | * @return string HTML |
407 | */ |
408 | protected function buildSummary( array $results ): string { |
409 | $statuses = []; |
410 | foreach ( $results as $result ) { |
411 | $status = strtolower( $result->getStatus() ); |
412 | $statuses[$status] = isset( $statuses[$status] ) ? $statuses[$status] + 1 : 1; |
413 | } |
414 | |
415 | $statusElements = []; |
416 | foreach ( $statuses as $status => $count ) { |
417 | if ( $count > 0 ) { |
418 | $statusElements[] = |
419 | $this->formatStatus( $status ) |
420 | . ': ' |
421 | . $count; |
422 | } |
423 | } |
424 | |
425 | return Html::rawElement( 'p', [], implode( ', ', $statusElements ) ); |
426 | } |
427 | |
428 | /** |
429 | * Formats given status to html |
430 | * |
431 | * @param string $status |
432 | * |
433 | * @throws InvalidArgumentException |
434 | * |
435 | * @return string HTML |
436 | */ |
437 | private function formatStatus( string $status ): string { |
438 | $messageName = "wbqc-constraintreport-status-" . strtolower( $status ); |
439 | $statusIcons = [ |
440 | CheckResult::STATUS_SUGGESTION => [ |
441 | 'icon' => 'suggestion-constraint-violation', |
442 | ], |
443 | CheckResult::STATUS_WARNING => [ |
444 | 'icon' => 'non-mandatory-constraint-violation', |
445 | ], |
446 | CheckResult::STATUS_VIOLATION => [ |
447 | 'icon' => 'mandatory-constraint-violation', |
448 | ], |
449 | CheckResult::STATUS_BAD_PARAMETERS => [ |
450 | 'icon' => 'alert', |
451 | 'flags' => 'warning', |
452 | ], |
453 | ]; |
454 | |
455 | if ( array_key_exists( $status, $statusIcons ) ) { |
456 | $iconWidget = new IconWidget( $statusIcons[$status] ); |
457 | $iconHtml = $iconWidget->toString() . ' '; |
458 | } else { |
459 | $iconHtml = ''; |
460 | } |
461 | |
462 | $labelWidget = new LabelWidget( [ |
463 | 'label' => $this->msg( $messageName )->text(), |
464 | ] ); |
465 | $labelHtml = $labelWidget->toString(); |
466 | |
467 | $formattedStatus = |
468 | Html::rawElement( |
469 | 'span', |
470 | [ |
471 | 'class' => 'wbqc-status wbqc-status-' . $status, |
472 | ], |
473 | $iconHtml . $labelHtml |
474 | ); |
475 | |
476 | return $formattedStatus; |
477 | } |
478 | |
479 | /** |
480 | * Returns html link to given entity with anchor to specified property. |
481 | * |
482 | * @param EntityId $entityId |
483 | * @param NumericPropertyId $propertyId |
484 | * @param string $text HTML |
485 | * |
486 | * @return string HTML |
487 | */ |
488 | private function getClaimLink( |
489 | EntityId $entityId, |
490 | NumericPropertyId $propertyId, |
491 | string $text |
492 | ): string { |
493 | return Html::rawElement( |
494 | 'a', |
495 | [ |
496 | 'href' => $this->getClaimUrl( $entityId, $propertyId ), |
497 | 'target' => '_blank', |
498 | ], |
499 | $text |
500 | ); |
501 | } |
502 | |
503 | /** |
504 | * Returns url of given entity with anchor to specified property. |
505 | */ |
506 | private function getClaimUrl( |
507 | EntityId $entityId, |
508 | NumericPropertyId $propertyId |
509 | ): string { |
510 | $title = $this->entityTitleLookup->getTitleForId( $entityId ); |
511 | $entityUrl = sprintf( '%s#%s', $title->getLocalURL(), $propertyId->getSerialization() ); |
512 | |
513 | return $entityUrl; |
514 | } |
515 | |
516 | } |