Dominic.sauer has submitted this change and it was merged.
Change subject: Added ViolationContext for cross-check violation for violations
special page.
......................................................................
Added ViolationContext for cross-check violation for violations special page.
Refactored top-level factory so that injection of Wikibase services is
possible. Now guard clauses are used in ImportContext. Moved
CheckResultTranslator to new Violations namespace.
Change-Id: I0fce5c48797de6ff01f840339bad07872642f855
---
M WikibaseQualityExternalValidation.php
M i18n/en.json
M i18n/qqq.json
M includes/ExternalValidationFactory.php
M includes/UpdateTable/ImportContext.php
R includes/Violations/CrossCheckResultToViolationTranslator.php
A includes/Violations/CrossCheckViolationContext.php
M maintenance/UpdateTable.php
M specials/SpecialCrossCheck.php
M tests/phpunit/ExternalValidationFactoryTest.php
M tests/phpunit/UpdateTable/ImportContextTest.php
R tests/phpunit/Violations/CrossCheckResultToViolationTranslatorTest.php
A tests/phpunit/Violations/CrossCheckViolationContextTest.php
A tests/phpunit/Violations/testdata/Q1.json
14 files changed, 769 insertions(+), 65 deletions(-)
Approvals:
Dominic.sauer: Verified; Looks good to me, approved
diff --git a/WikibaseQualityExternalValidation.php
b/WikibaseQualityExternalValidation.php
index d1637eb..7d0d500 100755
--- a/WikibaseQualityExternalValidation.php
+++ b/WikibaseQualityExternalValidation.php
@@ -33,7 +33,7 @@
// Define modules
$GLOBALS['wgResourceModules']['SpecialCrossCheckPage'] = array (
- 'styles' =>
'/modules/ext.WikibaseExternalValidation.SpecialCrossCheckPage.css',
+ 'styles' =>
'modules/ext.WikibaseExternalValidation.SpecialCrossCheckPage.css',
'localBasePath' => __DIR__,
'remoteExtPath' => 'WikibaseQualityExternalValidation'
);
@@ -46,4 +46,7 @@
// Jobs
$GLOBALS['wgDebugLogGroups']['wbq_evaluation'] =
'/var/log/mediawiki/wbq_evaluation.log';
$GLOBALS['wgJobClasses']['evaluateCrossCheckJob'] =
'WikibaseQuality\ExternalValidation\EvaluateCrossCheckJob';
+
+ // Register violation context
+ $GLOBALS['wbqViolationContexts'][] = function() { return
WikibaseQuality\ExternalValidation\ExternalValidationFactory::getDefaultInstance()->getViolationContext();
};
} );
\ No newline at end of file
diff --git a/i18n/en.json b/i18n/en.json
index 02468fe..e3399c0 100755
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -43,5 +43,11 @@
"apihelp-wbqevcrosscheck-examples-2": "Run cross-check for all statements of
item with ID Q76 and item with ID Q567.",
"apihelp-wbqevcrosscheck-examples-3": "Run cross-check for all statements of
item with ID Q76 and item with ID Q567, that uses property with ID P19.",
"apihelp-wbqevcrosscheck-examples-4": "Run cross-check for all statements of
item with ID Q76 and item with ID Q567, that uses property with ID P19 or P31.",
- "apihelp-wbqevcrosscheck-examples-5": "Run cross-check for claim with GUID
of Q42$D8404CDA-25E4-4334-AF13-A3290BCD9C0F."
+ "apihelp-wbqevcrosscheck-examples-5": "Run cross-check for claim with GUID
of Q42$D8404CDA-25E4-4334-AF13-A3290BCD9C0F.",
+
+ "wbqev-violations-group": "External Validation",
+ "wbqev-violation-header-external-source": "External source:",
+ "wbqev-violation-header-local-value": "Wikidata value:",
+ "wbqev-violation-header-external-values": "External values:",
+ "wbqev-violation-message": "Cross-Check with $1 has pointed out a violation.
Please click on icon for further information."
}
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 2d35052..c48eb84 100755
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -26,6 +26,7 @@
"wbqev-crosscheck-status-mismatch": "Status for claims that have a
mismatch with any external data.",
"wbqev-crosscheck-status-references-missing": "Status for claims for
which references are missing.",
"wbqev-crosscheck-status-references-stated": "Status for claims for
which references are stated.",
+
"wbqev-externaldbs": "{{doc-special|ExternalDbs}}",
"wbqev-externaldbs-instructions": "Purpose of SpecialPage:ExternalDbs",
"wbqev-externaldbs-overview-headline": "Headline that appears above the
overview of external databases.\n{{Identical|Database}}",
@@ -37,6 +38,7 @@
"wbqev-externaldbs-size": "Name for size from the external
source.\n{{Identical|Size}}",
"wbqev-externaldbs-license": "Name for license from the external
source.\n{{Identical|License}}",
"wbqev-externaldbs-no-databases": "Message that appears when no
external databases exist.",
+
"apihelp-wbqevcrosscheck-description":
"{{doc-apihelp-description|wblinktitles}}",
"apihelp-wbqevcrosscheck-param-entities":
"{{doc-apihelp-param|wbqevcrosscheck|entities}}",
"apihelp-wbqevcrosscheck-param-properties":
"{{doc-apihelp-param|wbqevcrosscheck|properties}}",
@@ -45,5 +47,11 @@
"apihelp-wbqevcrosscheck-examples-2":
"{{doc-apihelp-example|wbqevcrosscheck}}",
"apihelp-wbqevcrosscheck-examples-3":
"{{doc-apihelp-example|wbqevcrosscheck}}",
"apihelp-wbqevcrosscheck-examples-4":
"{{doc-apihelp-example|wbqevcrosscheck}}",
- "apihelp-wbqevcrosscheck-examples-5":
"{{doc-apihelp-example|wbqevcrosscheck}}"
+ "apihelp-wbqevcrosscheck-examples-5":
"{{doc-apihelp-example|wbqevcrosscheck}}",
+
+ "wbqev-violations-group": "Name of the group of external validation
violations. Is shown in special page that lists all the violations.",
+ "wbqev-violation-header-external-source": "Header for section in
violations special page that displays the name of the external source.",
+ "wbqev-violation-header-local-value": "Header for section in violations
special page that displays the data values stored in external databases.",
+ "wbqev-violation-header-external-values": "Header for section in
violations special page that displays the name of the external source.",
+ "wbqev-violation-message": "Message that is shown for violated claims on
item page. First parameter is name of the data source."
}
diff --git a/includes/ExternalValidationFactory.php
b/includes/ExternalValidationFactory.php
index e2a8335..bdad970 100755
--- a/includes/ExternalValidationFactory.php
+++ b/includes/ExternalValidationFactory.php
@@ -2,7 +2,22 @@
namespace WikibaseQuality\ExternalValidation;
+use RequestContext;
+use DataValues\Deserializers\DataValueDeserializer;
use DataValues\Serializers\DataValueSerializer;
+use ValueFormatters\FormatterOptions;
+use ValueFormatters\ValueFormatter;
+use Wikibase\DataModel\Claim\ClaimGuidParser;
+use Wikibase\DataModel\Entity\EntityIdParser;
+use Wikibase\Lib\EntityIdHtmlLinkFormatter;
+use Wikibase\Lib\LanguageNameLookup;
+use Wikibase\Lib\OutputFormatValueFormatterFactory;
+use Wikibase\Lib\SnakFormatter;
+use Wikibase\Lib\Store\EntityLookup;
+use Wikibase\Lib\Store\EntityRevisionLookup;
+use Wikibase\Lib\Store\EntityTitleLookup;
+use Wikibase\Lib\Store\LanguageLabelDescriptionLookup;
+use Wikibase\Lib\Store\TermLookup;
use Wikibase\Repo\WikibaseRepo;
use
WikibaseQuality\ExternalValidation\CrossCheck\Comparer\DataValueComparerFactory;
use WikibaseQuality\ExternalValidation\CrossCheck\CrossChecker;
@@ -10,7 +25,9 @@
use WikibaseQuality\ExternalValidation\CrossCheck\ReferenceHandler;
use
WikibaseQuality\ExternalValidation\DumpMetaInformation\DumpMetaInformationRepo;
use WikibaseQuality\ExternalValidation\Serializer\SerializerFactory;
-use
WikibaseQuality\ExternalValidation\CrossCheck\Result\CrossCheckResultToViolationTranslator;
+use WikibaseQuality\Violations\ViolationContext;
+use
WikibaseQuality\ExternalValidation\Violations\CrossCheckResultToViolationTranslator;
+use WikibaseQuality\ExternalValidation\Violations\CrossCheckViolationContext;
/**
@@ -21,6 +38,41 @@
* @license GNU GPL v2+
*/
class ExternalValidationFactory {
+
+ /**
+ * @var EntityLookup
+ */
+ private $entityLookup;
+
+ /**
+ * @var EntityRevisionLookup
+ */
+ private $entityRevisionLookup;
+
+ /**
+ * @var TermLookup
+ */
+ private $termLookup;
+
+ /**
+ * @var EntityTitleLookup
+ */
+ private $entityTitleLookup;
+
+ /**
+ * @var EntityIdParser
+ */
+ private $entityIdParser;
+
+ /**
+ * @var ClaimGuidParser
+ */
+ private $claimGuidParser;
+
+ /**
+ * @var ValueFormatter
+ */
+ private $valueFormatterFactory;
/**
* @var CrossChecker
@@ -58,6 +110,28 @@
private $crossCheckResultToViolationTranslator;
/**
+ * @param EntityLookup $entityLookup
+ * @param EntityRevisionLookup $entityRevisionLookup
+ * @param TermLookup $termLookup
+ * @param EntityTitleLookup $entityTitleLookup
+ * @param EntityIdParser $entityIdParser
+ * @param ClaimGuidParser $claimGuidParser
+ * @param OutputFormatValueFormatterFactory $valueFormatterFactory
+ */
+ public function __construct( EntityLookup $entityLookup,
EntityRevisionLookup $entityRevisionLookup,
+ TermLookup $termLookup, EntityTitleLookup
$entityTitleLookup,
+ EntityIdParser $entityIdParser,
ClaimGuidParser $claimGuidParser,
+ OutputFormatValueFormatterFactory
$valueFormatterFactory ) {
+ $this->entityLookup = $entityLookup;
+ $this->entityRevisionLookup = $entityRevisionLookup;
+ $this->termLookup = $termLookup;
+ $this->entityTitleLookup = $entityTitleLookup;
+ $this->entityIdParser = $entityIdParser;
+ $this->claimGuidParser = $claimGuidParser;
+ $this->valueFormatterFactory = $valueFormatterFactory;
+ }
+
+ /**
* Returns the default instance.
* IMPORTANT: Use only when it is not feasible to inject an instance
properly.
*
@@ -67,7 +141,16 @@
static $instance = null;
if ( $instance === null ) {
- $instance = new self();
+ $repo = WikibaseRepo::getDefaultInstance()->getDefaultInstance();
+ $instance = new self(
+ $repo->getEntityLookup(),
+ $repo->getEntityRevisionLookup(),
+ $repo->getTermLookup(),
+ $repo->getEntityTitleLookup(),
+ $repo->getEntityIdParser(),
+ $repo->getClaimGuidParser(),
+ $repo->getValueFormatterFactory()
+ );
}
return $instance;
@@ -95,11 +178,9 @@
*/
public function getCrossCheckInteractor() {
if ( $this->crossCheckInteractor === null ) {
- $entityLookup =
WikibaseRepo::getDefaultInstance()->getEntityLookup();
- $claimGuidParser =
WikibaseRepo::getDefaultInstance()->getClaimGuidParser();
$this->crossCheckInteractor = new CrossCheckInteractor(
- $entityLookup,
- $claimGuidParser,
+ $this->entityLookup,
+ $this->claimGuidParser,
$this->getCrossChecker() );
}
@@ -111,8 +192,7 @@
*/
public function getDataValueComparerFactory() {
if ( $this->dataValueComparerFactory === null ) {
- $entityLookup =
WikibaseRepo::getDefaultInstance()->getEntityLookup();
- $this->dataValueComparerFactory = new DataValueComparerFactory(
$entityLookup );
+ $this->dataValueComparerFactory = new DataValueComparerFactory(
$this->entityLookup );
}
return $this->dataValueComparerFactory;
@@ -126,7 +206,7 @@
$this->dumpMetaInformationRepo = new DumpMetaInformationRepo(
DUMP_META_TABLE,
DUMP_IDENTIFIER_PROPERTIES_TABLE,
- WikibaseRepo::getDefaultInstance()->getEntityIdParser()
+ $this->entityIdParser
);
}
@@ -164,10 +244,48 @@
* @return CrossCheckResultToViolationTranslator
*/
public function getCrossCheckResultToViolationTranslator() {
- if( $this->crossCheckResultToViolationTranslator === null ) {
- $this->crossCheckResultToViolationTranslator = new
CrossCheckResultToViolationTranslator(
WikibaseRepo::getDefaultInstance()->getEntityRevisionLookup() );
+ if ( $this->crossCheckResultToViolationTranslator === null ) {
+ $this->crossCheckResultToViolationTranslator = new
CrossCheckResultToViolationTranslator( $this->entityRevisionLookup );
}
return $this->crossCheckResultToViolationTranslator;
}
+
+ /**
+ * @return ViolationContext
+ */
+ public function getViolationContext() {
+ $languageCode = RequestContext::getMain()->getLanguage()->getCode();
+
+ $dataValueDeserializer = new DataValueDeserializer(
+ array (
+ 'boolean' => 'DataValues\BooleanValue',
+ 'number' => 'DataValues\NumberValue',
+ 'string' => 'DataValues\StringValue',
+ 'unknown' => 'DataValues\UnknownValue',
+ 'globecoordinate' => 'DataValues\GlobeCoordinateValue',
+ 'monolingualtext' => 'DataValues\MonolingualTextValue',
+ 'multilingualtext' => 'DataValues\MultilingualTextValue',
+ 'quantity' => 'DataValues\QuantityValue',
+ 'time' => 'DataValues\TimeValue',
+ 'wikibase-entityid' =>
'Wikibase\DataModel\Entity\EntityIdValue',
+ )
+ );
+ $labelLookup = new LanguageLabelDescriptionLookup( $this->termLookup,
$languageCode );
+ $entityIdLinkFormatter = new EntityIdHtmlLinkFormatter(
+ $labelLookup,
+ $this->entityTitleLookup,
+ new LanguageNameLookup()
+ );
+ $formatterOptions = new FormatterOptions();
+ $formatterOptions->setOption( SnakFormatter::OPT_LANG, $languageCode );
+ $dataValueFormatter = $this->valueFormatterFactory->getValueFormatter(
SnakFormatter::FORMAT_HTML, $formatterOptions );
+
+ return new CrossCheckViolationContext(
+ $dataValueDeserializer,
+ $entityIdLinkFormatter,
+ $dataValueFormatter,
+ $this->getDumpMetaInformationRepo()
+ );
+ }
}
\ No newline at end of file
diff --git a/includes/UpdateTable/ImportContext.php
b/includes/UpdateTable/ImportContext.php
index f67fc85..22c5775 100755
--- a/includes/UpdateTable/ImportContext.php
+++ b/includes/UpdateTable/ImportContext.php
@@ -62,11 +62,10 @@
* @param int $batchSize
*/
public function setBatchSize( $batchSize ) {
- if ( is_int( $batchSize ) ) {
- $this->batchSize = $batchSize;
- } else {
- throw new InvalidArgumentException( '$batchSize must be
of type int.' );
+ if ( !is_int( $batchSize ) ) {
+ throw new InvalidArgumentException( '$batchSize must be of type
int.' );
}
+ $this->batchSize = $batchSize;
}
/**
@@ -80,21 +79,19 @@
* @param boolean $quiet
*/
public function setQuiet( $quiet ) {
- if ( is_bool( $quiet ) ) {
- $this->quiet = $quiet;
- } else {
- throw new InvalidArgumentException( '$quiet must be of
type bool.' );
+ if ( !is_bool( $quiet ) ) {
+ throw new InvalidArgumentException( '$quiet must be of type bool.'
);
}
+ $this->quiet = $quiet;
}
/**
* @param string $tarFilePath
*/
public function setTarFilePath( $tarFilePath ) {
- if ( is_string( $tarFilePath ) ) {
- $this->tarFilePath = $tarFilePath;
- } else {
- throw new InvalidArgumentException( '$entitiesFilePath
must be of type string.' );
+ if ( !is_string( $tarFilePath ) ) {
+ throw new InvalidArgumentException( '$entitiesFilePath must be of
type string.' );
}
+ $this->tarFilePath = $tarFilePath;
}
}
\ No newline at end of file
diff --git
a/includes/CrossCheck/Result/CrossCheckResultToViolationTranslator.php
b/includes/Violations/CrossCheckResultToViolationTranslator.php
similarity index 77%
rename from includes/CrossCheck/Result/CrossCheckResultToViolationTranslator.php
rename to includes/Violations/CrossCheckResultToViolationTranslator.php
index 926985d..d427b13 100755
--- a/includes/CrossCheck/Result/CrossCheckResultToViolationTranslator.php
+++ b/includes/Violations/CrossCheckResultToViolationTranslator.php
@@ -1,10 +1,13 @@
<?php
-namespace WikibaseQuality\ExternalValidation\CrossCheck\Result;
+namespace WikibaseQuality\ExternalValidation\Violations;
use DataValues\Serializers\DataValueSerializer;
use Wikibase\DataModel\Entity\Entity;
use Wikibase\Lib\Store\EntityRevisionLookup;
+use WikibaseQuality\ExternalValidation\CrossCheck\Result\CompareResult;
+use WikibaseQuality\ExternalValidation\CrossCheck\Result\CrossCheckResult;
+use WikibaseQuality\ExternalValidation\CrossCheck\Result\CrossCheckResultList;
use WikibaseQuality\Violations\Violation;
@@ -41,9 +44,10 @@
//TODO: Use real ClaimGuid and TypeEntityId
$constraintTypeEntityId =
$crossCheckResult->getDumpMetaInformation()->getSourceItemId();
$constraintId = md5(
$crossCheckResult->getDumpMetaInformation()->getImportDate()->format( 'YmdHis'
) . $constraintTypeEntityId );
+ $constraintId = CrossCheckViolationContext::CONTEXT_ID .
Violation::CONSTRAINT_ID_DELIMITER . $constraintId;
$revisionId = $this->entityRevisionLookup->getLatestRevisionId(
$entityId );
- $status = CompareResult::STATUS_MISMATCH;
- $additionalInformation = $this->setAdditionalInformationJson(
$crossCheckResult );
+ $status = Violation::STATUS_VIOLATION;
+ $additionalInformation = $this->buildAdditionalInformation(
$crossCheckResult );
$violationArray[] = new Violation( $entityId, $propertyId,
$claimGuid, $constraintId, $constraintTypeEntityId, $revisionId, $status,
$additionalInformation);
}
@@ -51,7 +55,7 @@
return $violationArray;
}
- private function setAdditionalInformationJson( CrossCheckResult
$crossCheckResult ){
+ private function buildAdditionalInformation( CrossCheckResult
$crossCheckResult ){
$serializer = new DataValueSerializer();
$externalValues =
$crossCheckResult->getCompareResult()->getExternalValues();
@@ -64,6 +68,6 @@
'external_values' => $externalValuesSerialized
);
- return json_encode( $additionalInformation );
+ return $additionalInformation;
}
}
diff --git a/includes/Violations/CrossCheckViolationContext.php
b/includes/Violations/CrossCheckViolationContext.php
new file mode 100644
index 0000000..783f8df
--- /dev/null
+++ b/includes/Violations/CrossCheckViolationContext.php
@@ -0,0 +1,200 @@
+<?php
+
+namespace WikibaseQuality\ExternalValidation\Violations;
+
+
+use Deserializers\Deserializer;
+use Doctrine\Instantiator\Exception\InvalidArgumentException;
+use Html;
+use ValueFormatters\ValueFormatter;
+use Wikibase\DataModel\Entity\ItemId;
+use Wikibase\Lib\EntityIdFormatter;
+use WikibaseQuality\ExternalValidation\DumpMetaInformation\DumpMetaInformation;
+use
WikibaseQuality\ExternalValidation\DumpMetaInformation\DumpMetaInformationRepo;
+use WikibaseQuality\Violations\Violation;
+use WikibaseQuality\Violations\ViolationContext;
+
+
+class CrossCheckViolationContext implements ViolationContext {
+
+ const CONTEXT_ID = 'wbqev';
+
+ /**
+ * @var Deserializer
+ */
+ private $dataValueDeserializer;
+
+ /**
+ * @var EntityIdFormatter
+ */
+ private $entityIdFormatter;
+
+ /**
+ * @var ValueFormatter
+ */
+ private $valueFormatter;
+
+ /**
+ * @var DumpMetaInformationRepo
+ */
+ private $dumpMetaInformationRepo;
+
+ /**
+ * @param Deserializer $dataValueDeserializer
+ * @param EntityIdFormatter $entityIdFormatter
+ * @param ValueFormatter $valueFormatter
+ * @param DumpMetaInformationRepo $dumpMetaInformationRepo
+ */
+ public function __construct( Deserializer $dataValueDeserializer,
EntityIdFormatter $entityIdFormatter, ValueFormatter $valueFormatter,
DumpMetaInformationRepo $dumpMetaInformationRepo ) {
+ $this->dataValueDeserializer = $dataValueDeserializer;
+ $this->entityIdFormatter = $entityIdFormatter;
+ $this->valueFormatter = $valueFormatter;
+ $this->dumpMetaInformationRepo = $dumpMetaInformationRepo;
+ }
+
+ /**
+ * @see ViolationContext::getId
+ * @codeCoverageIgnore
+ *
+ * @return string
+ */
+ public function getId() {
+ return $this::CONTEXT_ID;
+ }
+
+ /**
+ * @see ViolationContext::getName
+ * @codeCoverageIgnore
+ *
+ * @return string
+ */
+ public function getName() {
+ return 'wbqev-violations-group';
+ }
+
+ /**
+ * @see ViolationContext::getTypes
+ *
+ * @return array
+ */
+ public function getTypes() {
+ $types = array();
+ $dumpMetaInformation = $this->dumpMetaInformationRepo->getALl();
+ foreach ( $dumpMetaInformation as $dump ) {
+ $type = $dump->getSourceItemId()->getSerialization(); //TODO:
return EntityId instead of serialization
+ if( !in_array( $type, $types ) ) {
+ $types[] = $type;
+ }
+ }
+
+ return $types;
+ }
+
+ /**
+ * @see ViolationContext::isContextFor
+ *
+ * @param Violation $violation
+ * @return bool
+ */
+ public function isContextFor( Violation $violation ) {
+ $prefix = explode( Violation::CONSTRAINT_ID_DELIMITER,
$violation->getConstraintId() )[0];
+
+ return $prefix === $this->getId();
+ }
+
+ /**
+ * @see ViolationContext::formatAdditionalInformation
+ *
+ * @param Violation $violation
+ * @return string
+ */
+ public function formatAdditionalInformation( Violation $violation ) {
+ if ( !$this->isContextFor( $violation ) ) {
+ throw new InvalidArgumentException( 'Given violation is not part
of current context.' );
+ }
+
+ $additionalInfo = $violation->getAdditionalInfo();
+ $output = $this->formatDataSource( $additionalInfo );
+ $output .= $this->formatExternalValues( $additionalInfo );
+
+ return $output;
+ }
+
+ /**
+ * @param array $additionalInformation
+ * @return string
+ */
+ private function formatDataSource( array $additionalInformation ) {
+ if ( array_key_exists( 'dump_id', $additionalInformation ) ) {
+ $dumpId = $additionalInformation['dump_id'];
+ $dumpMetaInformation = $this->dumpMetaInformationRepo->getWithId(
$dumpId );
+ $dataSourceEntityId = $dumpMetaInformation->getSourceItemId();
+ $dataSource = $this->entityIdFormatter->formatEntityId(
$dataSourceEntityId );
+
+ return $this->buildSection(
+ 'wbqev-violation-header-external-source',
+ $dataSource
+ );
+ }
+ }
+
+ /**
+ * @param array $additionalInformation
+ * @return string
+ */
+ private function formatExternalValues( array $additionalInformation ) {
+ if ( array_key_exists( 'external_values', $additionalInformation ) ) {
+ $externalValues = array_map(
+ function ( $serializedDataValue ) {
+ $dataValue = $this->dataValueDeserializer->deserialize(
$serializedDataValue );
+ return $this->valueFormatter->format( $dataValue );
+ },
+ $additionalInformation['external_values']
+ );
+
+ return $this->buildSection(
+ 'wbqev-violation-header-external-values',
+ implode( Html::element( 'br' ), $externalValues )
+ );
+ }
+ }
+
+ /**
+ * Build section for additional information output.
+ *
+ * @param string $headlineMessage
+ * @param string $content
+ * @return string
+ */
+ private function buildSection( $headlineMessage, $content ) {
+ return
+ Html::openElement( 'p' )
+ . Html::element(
+ 'span',
+ array(
+ 'class' => 'wbq-violations-additional-information-header'
+ ),
+ wfMessage( $headlineMessage )->text()
+ )
+ . Html::element( 'br' )
+ . $content
+ . Html::closeElement( 'p' );
+ }
+
+ /**
+ * @param Violation $violation
+ * @return string
+ */
+ public function getMessage( Violation $violation ) {
+ if ( !$this->isContextFor( $violation ) ) {
+ throw new InvalidArgumentException( 'Formatting of given violation
is not supported by current formatter.' );
+ }
+
+ $dataSourceEntityId = new ItemId(
$violation->getConstraintTypeEntityId() );
+ $dataSource = $this->entityIdFormatter->formatEntityId(
$dataSourceEntityId );
+
+ return wfMessage( 'wbqev-violation-message' )
+ ->params( $dataSource )
+ ->text();
+ }
+}
\ No newline at end of file
diff --git a/maintenance/UpdateTable.php b/maintenance/UpdateTable.php
index 5b01b55..8d263c9 100755
--- a/maintenance/UpdateTable.php
+++ b/maintenance/UpdateTable.php
@@ -47,6 +47,6 @@
}
// @codeCoverageIgnoreStart
-$maintClass = 'WikidataQuality\ExternalValidation\Maintenance\UpdateTable';
+$maintClass = 'WikibaseQuality\ExternalValidation\Maintenance\UpdateTable';
require_once RUN_MAINTENANCE_IF_MAIN;
// @codeCoverageIgnoreEnd
diff --git a/specials/SpecialCrossCheck.php b/specials/SpecialCrossCheck.php
index 7c26f07..dfdf1d8 100755
--- a/specials/SpecialCrossCheck.php
+++ b/specials/SpecialCrossCheck.php
@@ -5,6 +5,7 @@
use SpecialPage;
use ValueFormatters\FormatterOptions;
use ValueFormatters\ValueFormatter;
+use Wikibase\DataModel\Entity\Entity;
use Wikibase\DataModel\Entity\EntityIdParser;
use Wikibase\Lib\EntityIdLabelFormatter;
use Wikibase\Lib\EntityIdHtmlLinkFormatter;
@@ -32,7 +33,9 @@
use WikibaseQuality\ExternalValidation\ExternalValidationFactory;
use WikibaseQuality\Html\HtmlTable;
use WikibaseQuality\Html\HtmlTableHeader;
+use WikibaseQuality\Violations\ViolationRepo;
use WikibaseQuality\Violations\ViolationStore;
+use WikibaseQuality\WikibaseQualityFactory;
class SpecialCrossCheck extends SpecialPage {
@@ -78,6 +81,11 @@
private $crossCheckResultToViolationTranslator;
/**
+ * @var ViolationRepo
+ */
+ private $violationRepo;
+
+ /**
* @param string $name
* @param string $restriction
* @param bool $listed
@@ -109,6 +117,7 @@
$this->crossCheckInteractor =
ExternalValidationFactory::getDefaultInstance()->getCrossCheckInteractor();
$this->crossCheckResultToViolationTranslator =
ExternalValidationFactory::getDefaultInstance()->getCrossCheckResultToViolationTranslator();
+ $this->violationRepo =
WikibaseQualityFactory::getDefaultInstance()->getViolationRepo();
}
@@ -453,8 +462,9 @@
*/
protected function saveResultsInViolationsTable( $entity, $results ) {
$violations =
$this->crossCheckResultToViolationTranslator->translateToViolation( $entity,
$results );
- $violationStore = new ViolationStore();
- $violationStore->insertViolations( $violations );
+ foreach( $violations as $violation ) {
+ $this->violationRepo->save( $violation );
+ }
}
private function doEvaluation( $entity, $results ) {
@@ -465,6 +475,6 @@
$jobs[] = EvaluateCrossCheckJob::newInsertNow(
$entity->getId()->getSerialization(), $checkTimeStamp, $results );
$jobs[] = EvaluateCrossCheckJob::newInsertDeferred(
$entity->getId()->getSerialization(), $checkTimeStamp, 10*60 );
$jobs[] = EvaluateCrossCheckJob::newInsertDeferred(
$entity->getId()->getSerialization(), $checkTimeStamp, 60*60 );
- JobQueueGroup::singleton()->push( $jobs );
+ //JobQueueGroup::singleton()->push( $jobs );
}
}
diff --git a/tests/phpunit/ExternalValidationFactoryTest.php
b/tests/phpunit/ExternalValidationFactoryTest.php
index 5785462..1e5b2ce 100755
--- a/tests/phpunit/ExternalValidationFactoryTest.php
+++ b/tests/phpunit/ExternalValidationFactoryTest.php
@@ -70,12 +70,41 @@
$crossCheckResultToViolationTranslator =
$this->getFactory()->getCrossCheckResultToViolationTranslator();
$this->assertInstanceOf(
-
'WikibaseQuality\ExternalValidation\CrossCheck\Result\CrossCheckResultToViolationTranslator',
+
'WikibaseQuality\ExternalValidation\Violations\CrossCheckResultToViolationTranslator',
$crossCheckResultToViolationTranslator
);
}
+ public function testGetViolationContext() {
+ $violationContext = $this->getFactory()->getViolationContext();
+
+ $this->assertInstanceOf(
+ 'WikibaseQuality\Violations\ViolationContext',
+ $violationContext
+ );
+ }
+
private function getFactory() {
- return new ExternalValidationFactory();
+ return new ExternalValidationFactory(
+ $this->getMockForAbstractClass( 'Wikibase\Lib\Store\EntityLookup'
),
+ $this->getMockForAbstractClass(
'Wikibase\Lib\Store\EntityRevisionLookup' ),
+ $this->getMockForAbstractClass( 'Wikibase\Lib\Store\TermLookup' ),
+ $this->getMockForAbstractClass(
'Wikibase\Lib\Store\EntityTitleLookup' ),
+ $this->getMockForAbstractClass(
'Wikibase\DataModel\Entity\EntityIdParser' ),
+ $this->getMockBuilder( 'Wikibase\DataModel\Claim\ClaimGuidParser'
)->disableOriginalConstructor()->getMock(),
+ $this->getValueFormatterFactoryMock()
+ );
+ }
+
+ private function getValueFormatterFactoryMock() {
+ $mock = $this->getMockBuilder(
'Wikibase\Lib\OutputFormatValueFormatterFactory' )
+ ->setMethods( array( 'getValueFormatter' ) )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mock->expects( $this->any() )
+ ->method( 'getValueFormatter' )
+ ->willReturn( $this->getMockForAbstractClass(
'ValueFormatters\ValueFormatter' ) );
+
+ return $mock;
}
}
diff --git a/tests/phpunit/UpdateTable/ImportContextTest.php
b/tests/phpunit/UpdateTable/ImportContextTest.php
index ba2eb8f..67227fd 100755
--- a/tests/phpunit/UpdateTable/ImportContextTest.php
+++ b/tests/phpunit/UpdateTable/ImportContextTest.php
@@ -20,34 +20,28 @@
*
* @dataProvider provideInvalidArguments()
*/
- public function testConstructWithInvalidArguments( $loadBalancer,
$batchSize, $quiet, $tarFilePath ) {
+ public function testConstructWithInvalidArguments( $batchSize, $quiet,
$tarFilePath ) {
$this->setExpectedException( 'InvalidArgumentException' );
- new ImportContext( $loadBalancer, $batchSize, $quiet,
$tarFilePath );
+ new ImportContext( $batchSize, $quiet, $tarFilePath );
}
public function provideInvalidArguments() {
- $loadBalancer = $this->getMockBuilder( 'LoadBalancer' )
- ->disableOriginalConstructor()
- ->getMock();
return array(
array(
- $loadBalancer,
'invalidBatchSize',
true,
'entitiesFilePath'
),
array(
- $loadBalancer,
1234,
'invalidQuiet',
'entitiesFilePath'
),
- array(
- $loadBalancer,
- 1234,
- true,
- 1234
- )
+ array(
+ 1234,
+ true,
+ 1234
+ )
);
}
}
diff --git
a/tests/phpunit/CrossCheck/Result/CrossCheckResultToViolationTranslatorTest.php
b/tests/phpunit/Violations/CrossCheckResultToViolationTranslatorTest.php
similarity index 82%
rename from
tests/phpunit/CrossCheck/Result/CrossCheckResultToViolationTranslatorTest.php
rename to tests/phpunit/Violations/CrossCheckResultToViolationTranslatorTest.php
index 584a6e7..e87c8bc 100755
---
a/tests/phpunit/CrossCheck/Result/CrossCheckResultToViolationTranslatorTest.php
+++ b/tests/phpunit/Violations/CrossCheckResultToViolationTranslatorTest.php
@@ -1,6 +1,6 @@
<?php
-namespace WikibaseQuality\ExternalValidation\Test\CrossCheck\Result;
+namespace WikibaseQuality\ExternalValidation\Test\Violations;
use DataValues\StringValue;
use Wikibase\DataModel\Entity\Entity;
@@ -9,27 +9,27 @@
use Wikibase\DataModel\Entity\PropertyId;
use Wikibase\DataModel\Entity\ItemId;
use DateTime;
+use Wikibase\Repo\WikibaseRepo;
use WikibaseQuality\ExternalValidation\CrossCheck\Result\CompareResult;
-use WikibaseQuality\ExternalValidation\CrossCheck\Result\ReferenceResult;
use WikibaseQuality\ExternalValidation\CrossCheck\Result\CrossCheckResult;
use WikibaseQuality\ExternalValidation\CrossCheck\Result\CrossCheckResultList;
-use
WikibaseQuality\ExternalValidation\CrossCheck\Result\CrossCheckResultToViolationTranslator;
+use WikibaseQuality\ExternalValidation\CrossCheck\Result\ReferenceResult;
use WikibaseQuality\ExternalValidation\DumpMetaInformation\DumpMetaInformation;
-use Wikibase\Repo\WikibaseRepo;
+use
WikibaseQuality\ExternalValidation\Violations\CrossCheckResultToViolationTranslator;
/**
- * @covers
WikibaseQuality\ExternalValidation\CrossCheck\Result\CrossCheckResultToViolationTranslator
+ * @covers
WikibaseQuality\ExternalValidation\Violations\CrossCheckResultToViolationTranslator
*
- * @group WikibaseQualityExternalValidation
- * @group Database
- * @group medium
+ * @group WikibaseQualityExternalValidation
+ * @group Database
+ * @group medium
*
- * @uses
WikibaseQuality\ExternalValidation\DumpMetaInformation\DumpMetaInformation
- * @uses WikibaseQuality\ExternalValidation\CrossCheck\Result\CompareResult
- * @uses
WikibaseQuality\ExternalValidation\CrossCheck\Result\ReferenceResult
- * @uses
WikibaseQuality\ExternalValidation\CrossCheck\Result\CrossCheckResult
- * @uses
WikibaseQuality\ExternalValidation\CrossCheck\Result\CrossCheckResultList
+ * @uses
WikibaseQuality\ExternalValidation\DumpMetaInformation\DumpMetaInformation
+ * @uses WikibaseQuality\ExternalValidation\CrossCheck\Result\CompareResult
+ * @uses WikibaseQuality\ExternalValidation\CrossCheck\Result\ReferenceResult
+ * @uses
WikibaseQuality\ExternalValidation\CrossCheck\Result\CrossCheckResult
+ * @uses
WikibaseQuality\ExternalValidation\CrossCheck\Result\CrossCheckResultList
*
* @author BP2014N1
* @license GNU GPL v2+
@@ -138,9 +138,9 @@
$this->assertEquals( self::$idMap[ 'Q1' ], $violation->getEntityId() );
$this->assertEquals( 'P1',
$violation->getPropertyId()->getSerialization() );
$this->assertEquals( $this->claimGuid, $violation->getClaimGuid() );
- $this->assertEquals( md5(
$this->dumpMetaInformation->getImportDate()->format( 'YmdHis' ) .
$this->dumpMetaInformation->getSourceItemId() ), $violation->getConstraintId()
);
+ $this->assertEquals( 'wbqev|93346d9b0ea699c5387d1d2bf56322ff',
$violation->getConstraintId() );
$this->assertEquals( $this->dumpMetaInformation->getSourceItemId(),
$violation->getConstraintTypeEntityId() );
- $this->assertEquals(
'{"dump_id":"foo","external_values":[{"value":"test","type":"string"}]}',
$violation->getAdditionalInfo() );
+ $this->assertEquals( array( 'external_values' => array( array( 'value'
=> 'test', 'type' => 'string' ) ), 'dump_id' => 'foo' ),
$violation->getAdditionalInfo() );
$this->assertEquals( 42, $violation->getRevisionId() );
}
diff --git a/tests/phpunit/Violations/CrossCheckViolationContextTest.php
b/tests/phpunit/Violations/CrossCheckViolationContextTest.php
new file mode 100644
index 0000000..146cae0
--- /dev/null
+++ b/tests/phpunit/Violations/CrossCheckViolationContextTest.php
@@ -0,0 +1,313 @@
+<?php
+
+namespace WikibaseQuality\ExternalValidation\Test\Violations\Result;
+
+use DataValues\StringValue;
+use Language;
+use DataValues\DataValue;
+use Wikibase\DataModel\Entity\EntityId;
+use Wikibase\DataModel\Entity\ItemId;
+use WikibaseQuality\ExternalValidation\Violations\CrossCheckViolationContext;
+use WikibaseQuality\Tests\Helper\JsonFileEntityLookup;
+
+
+/**
+ * @covers
WikibaseQuality\ExternalValidation\Violations\CrossCheckViolationContext
+ *
+ * @group WikibaseQualityExternalValidation
+ *
+ * @author BP2014N1
+ * @license GNU GPL v2+
+ */
+class CrossCheckViolationContextTest extends \MediaWikiTestCase {
+
+ /**
+ * @var CrossCheckViolationContext
+ */
+ private $violationContext;
+
+ public function setUp() {
+ parent::setUp();
+
+ $dumpMetaInformation = array(
+ $this->getDumpMetaInformationMock( new ItemId( 'Q21' ) ),
+ $this->getDumpMetaInformationMock( new ItemId( 'Q42' ) ),
+ $this->getDumpMetaInformationMock( new ItemId( 'Q84' ) )
+ );
+
+ $this->violationContext = new CrossCheckViolationContext(
+ $this->getDataValueDeserializerMock(),
+ $this->getEntityIdFormatterMock(),
+ $this->getValueFormatterMock(),
+ $this->getDumpMetaInformationRepoMock( $dumpMetaInformation ),
+ new JsonFileEntityLookup( __DIR__ . '/testdata' )
+ );
+ }
+
+ public function tearDown() {
+ unset( $this->violationContext );
+
+ parent::tearDown();
+ }
+
+
+ public function testGetTypes() {
+ $actualResult = $this->violationContext->getTypes();
+ $expectedResult = array(
+ new ItemId( 'Q21' ),
+ new ItemId( 'Q42' ),
+ new ItemId( 'Q84' )
+ );
+
+ $this->assertArrayEquals( $expectedResult, $actualResult );
+ }
+
+
+ /**
+ * @dataProvider isContextForDataProvider
+ */
+ public function testIsContextFor( $expectedResult, $violation ) {
+ $actualResult = $this->violationContext->isContextFor( $violation );
+
+ $this->assertEquals( $expectedResult, $actualResult );
+ }
+
+ /**
+ * Test cases for testIsContextFor
+ * @return array
+ */
+ public function isContextForDataProvider() {
+ return array(
+ array(
+ true,
+ $this->getViolationMock( new ItemId( 'Q42' ), 'foobar',
'wbqev|foobar' )
+ ),
+ array(
+ false,
+ $this->getViolationMock( new ItemId( 'Q42' ), 'foobar',
'wbqc|foobar' )
+ ),
+ array(
+ false,
+ $this->getViolationMock( new ItemId( 'Q42' ), 'foobar',
'foobar' )
+ )
+ );
+ }
+
+
+ /**
+ * @dataProvider formatAdditionalInformationDataProvider
+ */
+ public function testFormatAdditionalInformation( $expectedResult,
$violation, $expectedException = null ) {
+ $this->setExpectedException( $expectedException );
+
+ global $wgLang;
+ $wgLang = Language::factory( 'qqx' );
+ $actualResult = $this->violationContext->formatAdditionalInformation(
$violation );
+
+ $this->assertEquals( $expectedResult, $actualResult );
+ }
+
+ /**
+ * Test cases for testFormatAdditionalInformation
+ * @return array
+ */
+ public function formatAdditionalInformationDataProvider() {
+ return array(
+ array(
+ '<p><span
class="wbq-violations-additional-information-header">(wbqev-violation-header-external-source)</span><br
/>Q42</p><p><span
class="wbq-violations-additional-information-header">(wbqev-violation-header-external-values)</span><br
/>foo</p>',
+ $this->getViolationMock(
+ new ItemId( 'Q1' ),
+ 'Q1$c0f25a6f-9e33-41c8-be34-c86a730ff30b',
+ 'wbqev|foobar',
+ 'Q42',
+ array(
+ 'dump_id' => 'foobar',
+ 'external_values' => array( 'foo' )
+ )
+ )
+ ),
+ array(
+ '<p><span
class="wbq-violations-additional-information-header">(wbqev-violation-header-external-source)</span><br
/>Q42</p><p><span
class="wbq-violations-additional-information-header">(wbqev-violation-header-external-values)</span><br
/>foo<br />bar</p>',
+ $this->getViolationMock(
+ new ItemId( 'Q1' ),
+ 'Q1$c0f25a6f-9e33-41c8-be34-c86a730ff30b',
+ 'wbqev|foobar',
+ 'Q42',
+ array(
+ 'dump_id' => 'foobar',
+ 'external_values' => array( 'foo', 'bar' )
+ )
+ )
+ ),
+ array(
+ null,
+ $this->getViolationMock(
+ new ItemId( 'Q1' ),
+ 'Q1$c0f25a6f-9e33-41c8-be34-c86a730ff30b',
+ 'wbqev|foobar',
+ 'Q42',
+ array()
+ )
+ ),
+ array(
+ null,
+ $this->getViolationMock(
+ new ItemId( 'Q1' ),
+ 'Q1$c0f25a6f-9e33-41c8-be34-c86a730ff30b',
+ 'wbqc|foobar',
+ 'Q42',
+ array()
+ ),
+ 'InvalidArgumentException'
+ )
+ );
+ }
+
+
+ /**
+ * @dataProvider getMessageDataProvider
+ */
+ public function testGetMessage( $expectedResult, $violation,
$expectedException = null ) {
+ $this->setExpectedException( $expectedException );
+
+ global $wgLang;
+ $wgLang = Language::factory( 'qqx' );
+ $actualResult = $this->violationContext->getMessage( $violation );
+
+ $this->assertEquals( $expectedResult, $actualResult );
+ }
+
+ /**
+ * Test cases for testGetMessage
+ * @return array
+ */
+ public function getMessageDataProvider() {
+ return array(
+ array(
+ '(wbqev-violation-message: Q42)',
+ $this->getViolationMock(
+ new ItemId( 'Q1' ),
+ 'foobar',
+ 'wbqev|foobar',
+ 'Q42'
+ )
+ ),
+ array(
+ '(wbqev-violation-message: Q84)',
+ $this->getViolationMock(
+ new ItemId( 'Q1' ),
+ 'foobar',
+ 'wbqev|foobar',
+ 'Q84'
+ )
+ ),
+ array(
+ null,
+ $this->getViolationMock(
+ new ItemId( 'Q1' ),
+ 'foobar',
+ 'wbqc|foobar',
+ 'Q84'
+ ),
+ 'InvalidArgumentException'
+ )
+ );
+ }
+
+
+ private function getDataValueDeserializerMock() {
+ $mock = $this->getMockBuilder( 'Deserializers\Deserializer' )
+ ->setMethods( array( 'deserialize' ) )
+ ->getMockForAbstractClass();
+ $mock->expects( $this->any() )
+ ->method( 'deserialize' )
+ ->willReturnCallback(
+ function( $value ) {
+ return new StringValue( $value );
+ }
+ );
+
+ return $mock;
+ }
+
+ private function getEntityIdFormatterMock() {
+ $mock = $this->getMockBuilder( 'Wikibase\Lib\EntityIdFormatter' )
+ ->setMethods( array( 'formatEntityId' ) )
+ ->getMockForAbstractClass();
+ $mock->expects( $this->any() )
+ ->method( 'formatEntityId' )
+ ->willReturnCallback(
+ function( EntityId $entityId ) {
+ return $entityId->getSerialization();
+ }
+ );
+
+ return $mock;
+ }
+
+ private function getValueFormatterMock() {
+ $mock = $this->getMockBuilder( 'ValueFormatters\ValueFormatter' )
+ ->setMethods( array( 'format' ) )
+ ->getMockForAbstractClass();
+ $mock->expects( $this->any() )
+ ->method( 'format' )
+ ->willReturnCallback(
+ function( DataValue $dataValue ) {
+ return $dataValue->getValue();
+ }
+ );
+
+ return $mock;
+ }
+
+ private function getDumpMetaInformationRepoMock( array
$dumpMetaInformation ) {
+ $mock = $this->getMockBuilder(
'WikibaseQuality\ExternalValidation\DumpMetaInformation\DumpMetaInformationRepo'
)
+ ->setMethods( array( 'getWithId', 'getAll' ) )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mock->expects( $this->any() )
+ ->method( 'getAll' )
+ ->willReturn( $dumpMetaInformation );
+ $mock->expects( $this->any() )
+ ->method( 'getWithId' )
+ ->willReturn( $this->getDumpMetaInformationMock( new ItemId( 'Q42'
) ) );
+
+ return $mock;
+ }
+
+ private function getDumpMetaInformationMock( EntityId $sourceItemId ) {
+ $mock = $this->getMockBuilder(
'WikibaseQuality\ExternalValidation\DumpMetaInformation\DumpMetaInformation' )
+ ->setMethods( array( 'getSourceItemId' ) )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mock->expects( $this->any() )
+ ->method( 'getSourceItemId' )
+ ->willReturn( $sourceItemId );
+
+ return $mock;
+ }
+
+ private function getViolationMock( $entityId = null, $claimGuid = null,
$constraintId = null, $constraintTypeEntityId = null, $additionalInformation =
null ) {
+ $mock = $this->getMockBuilder( 'WikibaseQuality\Violations\Violation' )
+ ->setMethods( array( 'getEntityId', 'getClaimGuid',
'getConstraintId', 'getConstraintTypeEntityId', 'getAdditionalInfo' ) )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mock->expects( $this->any() )
+ ->method( 'getEntityId' )
+ ->willReturn( $entityId );
+ $mock->expects( $this->any() )
+ ->method( 'getClaimGuid' )
+ ->willReturn( $claimGuid );
+ $mock->expects( $this->any() )
+ ->method( 'getConstraintId' )
+ ->willReturn( $constraintId );
+ $mock->expects( $this->any() )
+ ->method( 'getConstraintTypeEntityId' )
+ ->willReturn( $constraintTypeEntityId );
+ $mock->expects( $this->any() )
+ ->method( 'getAdditionalInfo' )
+ ->willReturn( $additionalInformation );
+
+ return $mock;
+ }
+}
diff --git a/tests/phpunit/Violations/testdata/Q1.json
b/tests/phpunit/Violations/testdata/Q1.json
new file mode 100644
index 0000000..942a8a7
--- /dev/null
+++ b/tests/phpunit/Violations/testdata/Q1.json
@@ -0,0 +1,22 @@
+{
+ "id": "Q1",
+ "type": "item",
+ "claims": {
+ "P1": [
+ {
+ "id": "Q1$c0f25a6f-9e33-41c8-be34-c86a730ff30b",
+ "mainsnak": {
+ "snaktype": "value",
+ "property": "P1",
+ "datatype": "string",
+ "datavalue": {
+ "value": "foo",
+ "type": "string"
+ }
+ },
+ "type": "statement",
+ "rank": "normal"
+ }
+ ]
+ }
+}
\ No newline at end of file
--
To view, visit https://gerrit.wikimedia.org/r/213820
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I0fce5c48797de6ff01f840339bad07872642f855
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/WikidataQualityExternalValidation
Gerrit-Branch: master
Gerrit-Owner: Soeren.oldag <[email protected]>
Gerrit-Reviewer: Dominic.sauer <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits