jenkins-bot has submitted this change and it was merged. Change subject: Add LexemeView ......................................................................
Add LexemeView Basic part Bug: T149499 Change-Id: Ie80f717f481ce0048f147d23ca0593998661f6a1 --- M WikibaseLexeme.entitytypes.php M extension.json A resources/__namespace.js A resources/datamodel/Lexeme.js A resources/datamodel/__namespace.js A resources/getDeserializer.js A resources/jquery.wikibase.lexemeview.js A resources/lexeme.css A resources/serialization/LexemeDeserializer.js A resources/serialization/__namespace.js A src/Actions/ViewLexemeAction.php M src/Content/LexemeHandler.php A src/View/LexemeView.php A tests/phpunit/mediawiki/View/LexemeViewTest.php 14 files changed, 662 insertions(+), 5 deletions(-) Approvals: WMDE-leszek: Looks good to me, approved jenkins-bot: Verified diff --git a/WikibaseLexeme.entitytypes.php b/WikibaseLexeme.entitytypes.php index d942217..0329013 100644 --- a/WikibaseLexeme.entitytypes.php +++ b/WikibaseLexeme.entitytypes.php @@ -11,6 +11,7 @@ * @license GPL-2.0+ * @author Amir Sarabadani <[email protected]> */ +use Wikibase\Repo\MediaWikiLanguageDirectionalityLookup; use Wikibase\Repo\WikibaseRepo; use Wikibase\DataModel\DeserializerFactory; use Wikibase\DataModel\SerializerFactory; @@ -27,6 +28,7 @@ use Wikibase\Lexeme\View\LexemeView; use Wikibase\View\EditSectionGenerator; use Wikibase\View\EntityTermsView; +use Wikibase\View\Template\TemplateFactory; return [ 'lexeme' => [ @@ -47,7 +49,20 @@ EditSectionGenerator $editSectionGenerator, EntityTermsView $entityTermsView ) { - return new LexemeView(); + $viewFactory = WikibaseRepo::getDefaultInstance()->getViewFactory(); + + return new LexemeView( + TemplateFactory::getDefaultInstance(), + $entityTermsView, + $viewFactory->newStatementSectionsView( + $languageCode, + $labelDescriptionLookup, + $fallbackChain, + $editSectionGenerator + ), + new MediaWikiLanguageDirectionalityLookup(), + $languageCode + ); }, 'content-model-id' => LexemeContent::CONTENT_MODEL_ID, 'content-handler-factory-callback' => function() { diff --git a/extension.json b/extension.json index fe4f4b1..8598f8e 100644 --- a/extension.json +++ b/extension.json @@ -35,8 +35,69 @@ "i18n" ] }, - "ResourceModules": {}, - "ResourceFileModulePaths": {}, + "ResourceModules": { + "jquery.wikibase.lexemeview": { + "scripts": "jquery.wikibase.lexemeview.js", + "dependencies": [ + "jquery.wikibase.entityview" + ] + }, + "wikibase.lexeme": { + "scripts": "__namespace.js", + "dependencies": "wikibase" + }, + "wikibase.lexeme.lexemeview": { + "dependencies": [ + "jquery.wikibase.lexemeview", + "wikibase.lexeme.getDeserializer", + "wikibase.lexeme.styles" + ] + }, + "wikibase.lexeme.datamodel.Lexeme": { + "scripts": [ + "datamodel/__namespace.js", + "datamodel/Lexeme.js" + ], + "dependencies": [ + "util.inherit", + "wikibase.datamodel.Entity", + "wikibase.datamodel.Fingerprint", + "wikibase.datamodel.StatementGroupSet", + "wikibase.datamodel.TermMap", + "wikibase.lexeme" + ] + }, + "wikibase.lexeme.getDeserializer": { + "scripts": "getDeserializer.js", + "dependencies": [ + "wikibase.lexeme.serialization.LexemeDeserializer" + ] + }, + "wikibase.lexeme.serialization.LexemeDeserializer": { + "scripts": [ + "serialization/__namespace.js", + "serialization/LexemeDeserializer.js" + ], + "dependencies": [ + "util.inherit", + "wikibase.lexeme", + "wikibase.lexeme.datamodel.Lexeme", + "wikibase.serialization.Deserializer", + "wikibase.serialization.StatementGroupSetDeserializer", + "wikibase.serialization.TermMapDeserializer" + ] + }, + "wikibase.lexeme.styles": { + "position": "top", + "styles": [ + "lexeme.css" + ] + } + }, + "ResourceFileModulePaths": { + "localBasePath": "resources", + "remoteExtPath": "WikibaseLexeme/resources" + }, "SpecialPages": { "NewLexeme": "Wikibase\\Lexeme\\Specials\\SpecialNewLexeme" }, diff --git a/resources/__namespace.js b/resources/__namespace.js new file mode 100644 index 0000000..49325f4 --- /dev/null +++ b/resources/__namespace.js @@ -0,0 +1 @@ +wikibase.lexeme = wikibase.lexeme || {}; diff --git a/resources/datamodel/Lexeme.js b/resources/datamodel/Lexeme.js new file mode 100644 index 0000000..c6abf2e --- /dev/null +++ b/resources/datamodel/Lexeme.js @@ -0,0 +1,97 @@ +( function( wb, util ) { + 'use strict'; + + var PARENT = wb.datamodel.Entity; + + /** + * @class wikibase.datamodel.Lexeme + * @extends wikibase.datamodel.Entity + * @license GNU GPL v2+ + * @author Adrian Heine <[email protected]> + * @todo Remove Fingerprint from Entity.js then remove it from here. + * + * @constructor + * + * @param {string} lexemeId + * @param {wikibase.datamodel.TermMap|null} [labels=new wikibase.datamodel.TermMap()] + * @param {wikibase.datamodel.StatementGroupSet|null} [statementGroupSet=new wikibase.datamodel.StatementGroupSet()] + * + * @throws {Error} if a required parameter is not specified properly. + */ + var SELF = wb.datamodel.Lexeme = util.inherit( + 'WbDataModelLexeme', + PARENT, + function( lexemeId, labels, statementGroupSet ) { + labels = labels || new wb.datamodel.TermMap(); + statementGroupSet = statementGroupSet || new wb.datamodel.StatementGroupSet(); + + if ( + typeof lexemeId !== 'string' || + !( labels instanceof wb.datamodel.TermMap ) || + !( statementGroupSet instanceof wb.datamodel.StatementGroupSet ) + ) { + throw new Error( 'Required parameter(s) missing or not defined properly' ); + } + + this._id = lexemeId; + this._statementGroupSet = statementGroupSet; + this._fingerprint = new wb.datamodel.Fingerprint( labels, new wb.datamodel.TermMap() ); + }, + { + + /** + * @property {wikibase.datamodel.StatementGroupSet} + * @private + */ + _statementGroupSet: null, + + /** + * @return {wikibase.datamodel.StatementGroupSet} + */ + getStatements: function() { + return this._statementGroupSet; + }, + + /** + * @param {wikibase.datamodel.Statement} statement + */ + addStatement: function( statement ) { + this._statementGroupSet.addStatement( statement ); + }, + + /** + * @param {wikibase.datamodel.Statement} statement + */ + removeStatement: function( statement ) { + this._statementGroupSet.removeStatement( statement ); + }, + + /** + * @return {boolean} + */ + isEmpty: function() { + return this._statementGroupSet.isEmpty() && this._fingerprint.isEmpty(); + }, + + /** + * @param {*} lexeme + * @return {boolean} + */ + equals: function( lexeme ) { + return lexeme === this || + ( lexeme instanceof SELF && + this._id === lexeme.getId() && + this._statementGroupSet.equals( lexeme.getStatements() ) && + this._fingerprint.equals( lexeme.getFingerprint() ) + ); + } + } ); + + /** + * @inheritdoc + * @property {string} [TYPE='lexeme'] + * @static + */ + SELF.TYPE = 'lexeme'; + +}( wikibase, util ) ); diff --git a/resources/datamodel/__namespace.js b/resources/datamodel/__namespace.js new file mode 100644 index 0000000..6b99c17 --- /dev/null +++ b/resources/datamodel/__namespace.js @@ -0,0 +1 @@ +wikibase.lexeme.datamodel = wikibase.lexeme.datamodel || {}; diff --git a/resources/getDeserializer.js b/resources/getDeserializer.js new file mode 100644 index 0000000..129a467 --- /dev/null +++ b/resources/getDeserializer.js @@ -0,0 +1,6 @@ +( function( wb ) { + 'use strict'; + module.exports = function() { + return new wb.lexeme.serialization.LexemeDeserializer(); + }; +}( wikibase ) ); diff --git a/resources/jquery.wikibase.lexemeview.js b/resources/jquery.wikibase.lexemeview.js new file mode 100644 index 0000000..fe3b4cc --- /dev/null +++ b/resources/jquery.wikibase.lexemeview.js @@ -0,0 +1,115 @@ +( function( $ ) { + 'use strict'; + + var PARENT = $.wikibase.entityview; + + /** + * View for displaying a Wikibase `Lexeme`. + * Copied from jQuery.wikibase.mediainfoview + * @class jQuery.wikibase.lexemeview + * @extends jQuery.wikibase.entityview + * @license GPL-2.0+ + * @author Adrian Heine + * + * @param {Object} options + * @param {Function} options.buildStatementGroupListView + * + * @constructor + * + */ + $.widget( 'wikibase.lexemeview', PARENT, { + /** + * @inheritdoc + * @protected + */ + options: { + buildStatementGroupListView: null + }, + + /** + * @property {jQuery} + * @readonly + */ + $statements: null, + + /** + * @inheritdoc + * @protected + */ + _create: function() { + this._createEntityview(); + + this.$statements = $( '.wikibase-statementgrouplistview', this.element ); + if ( this.$statements.length === 0 ) { + this.$statements = $( '<div/>' ).appendTo( this.$main ); + } + }, + + /** + * @inheritdoc + * @protected + */ + _init: function() { + if ( !this.options.buildStatementGroupListView ) { + throw new Error( 'Required option(s) missing' ); + } + + this._initStatements(); + PARENT.prototype._init.call( this ); + }, + + /** + * @protected + */ + _initStatements: function() { + this.options.buildStatementGroupListView( this.options.value, this.$statements ); + + // This is here to be sure there is never a duplicate id: + $( '.wikibase-statementgrouplistview' ) + .prev( '.wb-section-heading' ) + .first() + .attr( 'id', 'claims' ); + }, + + /** + * @inheritdoc + * @protected + */ + _attachEventHandlers: function() { + PARENT.prototype._attachEventHandlers.call( this ); + + var self = this; + + this.element + .on( [ + 'statementviewafterstartediting.' + this.widgetName, + 'referenceviewafterstartediting.' + this.widgetName + ].join( ' ' ), + function() { + self._trigger( 'afterstartediting' ); + } ); + + this.element + .on( [ + 'statementlistviewafterremove.' + this.widgetName, + 'statementviewafterstopediting.' + this.widgetName, + 'statementviewafterremove.' + this.widgetName, + 'referenceviewafterstopediting.' + this.widgetName + ].join( ' ' ), + function( event, dropValue ) { + self._trigger( 'afterstopediting', null, [ dropValue ] ); + } ); + }, + + /** + * @inheritdoc + * @protected + */ + _setState: function( state ) { + PARENT.prototype._setState.call( this, state ); + + this.$statements.data( 'statementgrouplistview' )[ state ](); + } + } ); + +}( jQuery ) ); diff --git a/resources/lexeme.css b/resources/lexeme.css new file mode 100644 index 0000000..fb69c32 --- /dev/null +++ b/resources/lexeme.css @@ -0,0 +1,14 @@ +.wb-lexeme.wikibase-entityview { + float: none; +} + +.wb-lexeme .wikibase-entitytermsforlanguagelistview-aliases, +.wb-lexeme .wikibase-entitytermsforlanguageview-aliases, +.wb-lexeme .wikibase-entitytermsview-aliases .wikibase-entitytermsforlanguagelistview-description { + display: none; +} + +.wb-lexeme .wikibase-entitytermsforlanguagelistview .wikibase-entitytermsforlanguagelistview-header .wikibase-entitytermsforlanguagelistview-header-row { + border-right: 0; + width: 60%; +} diff --git a/resources/serialization/LexemeDeserializer.js b/resources/serialization/LexemeDeserializer.js new file mode 100644 index 0000000..27cae62 --- /dev/null +++ b/resources/serialization/LexemeDeserializer.js @@ -0,0 +1,40 @@ +( function( wb, util ) { + 'use strict'; + + var MODULE = wb.lexeme.serialization, + SERIALIZER = wb.serialization, + PARENT = SERIALIZER.Deserializer; + + /** + * @class wikibase.serialization.LexemeDeserializer + * @extends wikibase.serialization.Deserializer + * @license GNU GPL v2+ + * @author Adrian Heine <[email protected]> + * + * @constructor + */ + MODULE.LexemeDeserializer = util.inherit( 'WbLexemeDeserializer', PARENT, { + /** + * @inheritdoc + * + * @return {wikibase.datamodel.Lexeme} + * + * @throws {Error} if serialization does not resolve to a serialized Lexeme. + */ + deserialize: function( serialization ) { + if ( serialization.type !== wb.datamodel.Lexeme.TYPE ) { + throw new Error( 'Serialization does not resolve to a Lexeme' ); + } + + var statementGroupSetDeserializer = new SERIALIZER.StatementGroupSetDeserializer(), + termMapDeserializer = new SERIALIZER.TermMapDeserializer(); + + return new wikibase.datamodel.Lexeme( + serialization.id, + termMapDeserializer.deserialize( serialization.labels ), + statementGroupSetDeserializer.deserialize( serialization.statements ) + ); + } + } ); + +}( wikibase, util ) ); diff --git a/resources/serialization/__namespace.js b/resources/serialization/__namespace.js new file mode 100644 index 0000000..dc72a20 --- /dev/null +++ b/resources/serialization/__namespace.js @@ -0,0 +1 @@ +wikibase.lexeme.serialization = wikibase.lexeme.serialization || {}; diff --git a/src/Actions/ViewLexemeAction.php b/src/Actions/ViewLexemeAction.php new file mode 100644 index 0000000..d5eacf1 --- /dev/null +++ b/src/Actions/ViewLexemeAction.php @@ -0,0 +1,21 @@ +<?php + +namespace Wikibase\Lexeme\Actions; + +use Wikibase\ViewEntityAction; + +/** + * Handles the view action for Wikibase Lexeme. + * + * @license GPL-2.0+ + * @author Amir Sarabadani <[email protected]> + */ +class ViewLexemeAction extends ViewEntityAction { + + function show() { + parent::show(); + $this->getOutput()->addJsConfigVars( 'wbUserSpecifiedLanguages', [] ); + $this->getOutput()->addModules( 'wikibase.lexeme.lexemeview' ); + } + +} diff --git a/src/Content/LexemeHandler.php b/src/Content/LexemeHandler.php index 58ff67c..6443419 100644 --- a/src/Content/LexemeHandler.php +++ b/src/Content/LexemeHandler.php @@ -7,9 +7,9 @@ use Wikibase\DataModel\Entity\EntityIdParser; use Wikibase\EditEntityAction; use Wikibase\HistoryEntityAction; -use Wikibase\ViewEntityAction; use Wikibase\Lib\Store\EntityContentDataCodec; use Wikibase\Lib\Store\LanguageFallbackLabelDescriptionLookupFactory; +use Wikibase\Lexeme\Actions\ViewLexemeAction; use Wikibase\Lexeme\DataModel\Lexeme; use Wikibase\Lexeme\DataModel\LexemeId; use Wikibase\Repo\Content\EntityHandler; @@ -86,7 +86,7 @@ $this->labelLookupFactory->newLabelDescriptionLookup( $context->getLanguage() ) ); }, - 'view' => ViewEntityAction::class, + 'view' => ViewLexemeAction::class, 'edit' => EditEntityAction::class, 'submit' => SubmitEntityAction::class, ]; diff --git a/src/View/LexemeView.php b/src/View/LexemeView.php new file mode 100644 index 0000000..66fa6a1 --- /dev/null +++ b/src/View/LexemeView.php @@ -0,0 +1,88 @@ +<?php + +namespace Wikibase\Lexeme\View; + +use InvalidArgumentException; +use MediaWiki\Linker\LinkRenderer; +use Wikibase\DataModel\Entity\EntityDocument; +use Wikibase\Lexeme\DataModel\Lexeme; +use Wikibase\Lexeme\DataModel\LexemeId; +use Wikibase\View\EntityTermsView; +use Wikibase\View\EntityView; +use Wikibase\View\LanguageDirectionalityLookup; +use Wikibase\View\StatementSectionsView; +use Wikibase\View\Template\TemplateFactory; +use Wikimedia\Assert\Assert; + +/** + * Class for creating HTML views for Lexeme instances. + * + * @license GPL-2.0+ + * @author Amir Sarabadani <[email protected]> + */ +class LexemeView extends EntityView { + + /** + * @var StatementSectionsView + */ + private $statementSectionsView; + + /** + * @param TemplateFactory $templateFactory + * @param EntityTermsView $entityTermsView + * @param StatementSectionsView $statementSectionsView + * @param LanguageDirectionalityLookup $languageDirectionalityLookup + * @param string $languageCode + */ + public function __construct( + TemplateFactory $templateFactory, + EntityTermsView $entityTermsView, + StatementSectionsView $statementSectionsView, + LanguageDirectionalityLookup $languageDirectionalityLookup, + $languageCode + ) { + parent::__construct( + $templateFactory, + $entityTermsView, + $languageDirectionalityLookup, + $languageCode + ); + + $this->statementSectionsView = $statementSectionsView; + } + + /** + * @see EntityView::getMainHtml + * + * @param EntityDocument $entity + * + * @throws InvalidArgumentException + * @return string HTML + */ + protected function getMainHtml( EntityDocument $entity ) { + Assert::parameterType( Lexeme::class, $entity, '$entity' ); + + $html = $this->getHtmlForLexicalCategoryAndLanguage( $entity ) + . $this->templateFactory->render( 'wikibase-toc' ) + . $this->statementSectionsView->getHtml( $entity->getStatements() ); + + return $html; + } + + /** + * @see EntityView::getSideHtml + * + * @param EntityDocument $entity + * + * @return string HTML + */ + protected function getSideHtml( EntityDocument $entity ) { + return ''; + } + + private function getHtmlForLexicalCategoryAndLanguage( $entity ) { + // TODO: Implement when building LexicalCategory and Language + return ''; + } + +} diff --git a/tests/phpunit/mediawiki/View/LexemeViewTest.php b/tests/phpunit/mediawiki/View/LexemeViewTest.php new file mode 100644 index 0000000..8230632 --- /dev/null +++ b/tests/phpunit/mediawiki/View/LexemeViewTest.php @@ -0,0 +1,197 @@ +<?php + +namespace Wikibase\Lexeme\Tests\MediaWiki\View; + +use InvalidArgumentException; +use MediaWiki\Linker\LinkRenderer; +use PHPUnit_Framework_TestCase; +use Wikibase\DataModel\Entity\EntityDocument; +use Wikibase\DataModel\Entity\PropertyId; +use Wikibase\DataModel\Snak\PropertyNoValueSnak; +use Wikibase\DataModel\Statement\Statement; +use Wikibase\DataModel\Statement\StatementList; +use Wikibase\Lexeme\DataModel\Lexeme; +use Wikibase\Lexeme\DataModel\LexemeId; +use Wikibase\Lexeme\View\LexemeView; +use Wikibase\View\EntityTermsView; +use Wikibase\View\EntityView; +use Wikibase\View\LanguageDirectionalityLookup; +use Wikibase\View\StatementSectionsView; +use Wikibase\View\Template\TemplateFactory; + +/** + * @covers Wikibase\Lexeme\View\LexemeView + * + * @group WikibaseLexeme + * + * @license GPL-2.0+ + * @author Amir Sarabadani <[email protected]> + */ +class LexemeViewTest extends PHPUnit_Framework_TestCase { + + /** + * @return StatementSectionsView + */ + private function newStatementSectionsViewMock() { + return $this->getMockBuilder( StatementSectionsView::class ) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * @return EntityTermsView + */ + private function newEntityTermsViewMock() { + return $this->getMockBuilder( EntityTermsView::class ) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * @return LanguageDirectionalityLookup + */ + private function newLanguageDirectionalityLookupMock() { + $languageDirectionalityLookup = $this->getMock( LanguageDirectionalityLookup::class ); + $languageDirectionalityLookup->method( 'getDirectionality' ) + ->willReturn( 'auto' ); + + return $languageDirectionalityLookup; + } + + private function newLexemeView( + $contentLanguageCode = 'en', + EntityTermsView $entityTermsView = null, + StatementSectionsView $statementSectionsView = null + ) { + $templateFactory = TemplateFactory::getDefaultInstance(); + + if ( !$entityTermsView ) { + $entityTermsView = $this->newEntityTermsViewMock(); + } + + if ( !$statementSectionsView ) { + $statementSectionsView = $this->newStatementSectionsViewMock(); + } + + /** @var LinkRenderer $linkRenderer */ + $linkRenderer = $this->getMockBuilder( LinkRenderer::class ) + ->disableOriginalConstructor() + ->getMock(); + + return new LexemeView( + $templateFactory, + $entityTermsView, + $statementSectionsView, + $this->newLanguageDirectionalityLookupMock(), + $contentLanguageCode, + $linkRenderer + ); + } + + public function testInstantiate() { + $view = $this->newLexemeView(); + $this->assertInstanceOf( LexemeView::class, $view ); + $this->assertInstanceOf( EntityView::class, $view ); + } + + public function testGetHtml_invalidEntityType() { + $view = $this->newLexemeView(); + + /** @var EntityDocument $entity */ + $entity = $this->getMock( EntityDocument::class ); + + $this->setExpectedException( InvalidArgumentException::class ); + $view->getHtml( $entity ); + } + + /** + * @dataProvider provideTestGetHtml + */ + public function testGetHtml( + Lexeme $entity, + LexemeId $entityId = null, + $contentLanguageCode = 'en', + StatementList $statements = null + ) { + $entityTermsView = $this->newEntityTermsViewMock(); + // TODO: Change this to Lexical category and language later + $entityTermsView + ->method( 'getHtml' ) + ->with( + $contentLanguageCode, + $entity, + $entity, + null, + $entityId + ) + ->will( $this->returnValue( 'entityTermsView->getHtml' ) ); + + $entityTermsView->expects( $this->never() ) + ->method( 'getEntityTermsForLanguageListView' ); + + $statementSectionsView = $this->newStatementSectionsViewMock(); + $statementSectionsView + ->method( 'getHtml' ) + ->with( + $this->callback( function( StatementList $statementList ) use ( $statements ) { + return $statements ? $statementList === $statements : $statementList->isEmpty(); + } ) + ) + ->will( $this->returnValue( 'statementSectionsView->getHtml' ) ); + + $view = $this->newLexemeView( + $contentLanguageCode, + $entityTermsView, + $statementSectionsView + ); + + $result = $view->getHtml( $entity ); + $this->assertInternalType( 'string', $result ); + $this->assertContains( 'wb-lexeme', $result ); + + } + + public function provideTestGetHtml() { + $lexemeId = new LexemeId( 'L1' ); + $statements = new StatementList( [ + new Statement( new PropertyNoValueSnak( new PropertyId( 'P1' ) ) ) + ] ); + + return [ + [ + new Lexeme() + ], + [ + new Lexeme( $lexemeId ), + $lexemeId + ], + [ + new Lexeme( $lexemeId, null, $statements ), + $lexemeId, + 'en', + $statements + ], + [ + new Lexeme( $lexemeId ), + $lexemeId, + 'lkt' + ], + [ + new Lexeme( $lexemeId, null, $statements ), + $lexemeId, + 'lkt', + $statements + ], + ]; + } + + public function testGetTitleHtml_invalidEntityType() { + $view = $this->newLexemeView(); + + /** @var EntityDocument $entity */ + $entity = $this->getMock( EntityDocument::class ); + $html = $view->getTitleHtml( $entity ); + $this->assertSame( $html, '' ); + } + +} -- To view, visit https://gerrit.wikimedia.org/r/319631 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Ie80f717f481ce0048f147d23ca0593998661f6a1 Gerrit-PatchSet: 13 Gerrit-Project: mediawiki/extensions/WikibaseLexeme Gerrit-Branch: master Gerrit-Owner: Ladsgroup <[email protected]> Gerrit-Reviewer: Daniel Kinzler <[email protected]> Gerrit-Reviewer: Jakob <[email protected]> Gerrit-Reviewer: Jeroen De Dauw <[email protected]> Gerrit-Reviewer: Jonas Kress (WMDE) <[email protected]> Gerrit-Reviewer: Ladsgroup <[email protected]> Gerrit-Reviewer: Thiemo Mättig (WMDE) <[email protected]> Gerrit-Reviewer: WMDE-leszek <[email protected]> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
