jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/402868 )
Change subject: Add API action for editing elements of a form
......................................................................
Add API action for editing elements of a form
Co-authored with Amir Sarabadani.
Bug: T184409
Change-Id: Ice096f53a09b80aa26d2ced342a7986b82d01998
---
M extension.json
M i18n/en.json
M i18n/qqq.json
A src/Api/EditFormElements.php
A src/Api/EditFormElementsRequest.php
A src/Api/EditFormElementsRequestParser.php
A src/Api/EditFormElementsRequestParserResult.php
A src/Api/EditFormElementsSummary.php
A src/Api/Error/FormNotFound.php
A src/Api/Error/ParameterIsNotFormId.php
A src/ChangeOp/ChangeOpEditFormElements.php
M src/DataModel/Form.php
A tests/phpunit/mediawiki/Api/EditFormElementsTest.php
13 files changed, 1,188 insertions(+), 0 deletions(-)
Approvals:
WMDE-leszek: Looks good to me, approved
jenkins-bot: Verified
diff --git a/extension.json b/extension.json
index 324d685..4acbf30 100644
--- a/extension.json
+++ b/extension.json
@@ -462,6 +462,10 @@
"wblexemeaddform": {
"class": "Wikibase\\Lexeme\\Api\\AddForm",
"factory":
"Wikibase\\Lexeme\\Api\\AddForm::newFromGlobalState"
+ },
+ "wblexemeeditformelements": {
+ "class": "Wikibase\\Lexeme\\Api\\EditFormElements",
+ "factory":
"Wikibase\\Lexeme\\Api\\EditFormElements::newFromGlobalState"
}
},
"manifest_version": 2
diff --git a/i18n/en.json b/i18n/en.json
index f90a514..5e8a788 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -35,13 +35,20 @@
"apihelp-wblexemeaddform-param-data": "JSON encoded data for Form i.e.
representations and grammaticalFeatures",
"apihelp-wblexemeaddform-param-bot": "Mark this edit as bot. This URL
flag will only be respected if the user belongs to the group
\"{{int:group-bot}}\".",
"apihelp-wblexemeaddform-example-1": "Add Form to the Lexeme with ID
<samp>$1</samp> having representations <samp>$2</samp> with languages
<samp>$3</samp> respectively and grammatical features <samp>$4</samp>",
+ "apihelp-wblexemeeditformelements-summary": "Edits representations and
grammatical features of a Form",
+ "apihelp-wblexemeeditformelements-param-formId": "ID of the Form, e.g.
L10-F2",
+ "apihelp-wblexemeeditformelements-param-data": "JSON encoded
representation and grammatical feature data",
+ "apihelp-wblexemeeditformelements-param-bot": "Mark this edit as bot.
This URL flag will only be respected if the user belongs to the group
\"{{int:group-bot}}\".",
+ "apihelp-wblexemeeditformelements-example-1": "Edits form with the ID
<samp>$1</samp> to have representations <samp>$2</samp> in languages
<samp>$3</samp> respectively, and grammatical features <samp>$4</samp>.",
"wikibase-lexeme-api-error-parameter-required": "Parameter \"$1\" is
required",
"wikibase-lexeme-api-error-parameter-invalid-json-object": "Parameter
\"$1\" expected to be a valid JSON object, given \"$2\"",
"wikibase-lexeme-api-error-lexeme-not-found": "No Lexeme with ID \"$1\"
was found.",
+ "wikibaselexeme-api-error-form-not-found": "No Form with ID \"$1\" was
found.",
"wikibase-lexeme-api-error-json-field-required": "Field \"$2\" in
parameter \"$1\" is required",
"wikibase-lexeme-api-error-json-field-not-item-id": "Field \"$2\" in
parameter \"$1\" expected to be na Item Id. Given: \"$3\"",
"wikibase-lexeme-api-error-json-field-has-wrong-type": "Field \"$2\" in
parameter \"$1\" expected to be of type \"$3\". Given: \"$4\"",
"wikibase-lexeme-api-error-parameter-not-lexeme-id": "Parameter \"$1\"
expected to be a Lexeme ID. Given: \"$2\"",
+ "wikibaselexeme-api-error-parameter-not-form-id": "Parameter \"$1\"
expected to be a Form ID. Given: \"$2\"",
"wikibase-lexeme-api-error-representation-text-cannot-be-empty":
"Representation text cannot be empty",
"wikibase-lexeme-api-error-representation-language-cannot-be-empty":
"Representation language cannot be empty",
"wikibase-lexeme-api-error-form-must-have-at-least-one-representation":
"Form must have at least one representation",
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 003fa5e..93acecd 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -40,13 +40,20 @@
"apihelp-wblexemeaddform-param-data":
"{{doc-apihelp-param|wblexemeaddform|data}}",
"apihelp-wblexemeaddform-param-bot":
"{{doc-apihelp-param|wblexemeaddform|bot}}",
"apihelp-wblexemeaddform-example-1":
"{{doc-apihelp-example|wblexemeaddform}}",
+ "apihelp-wblexemeeditformelements-summary":
"{{doc-apihelp-summary|wblexemeeditformelements}}",
+ "apihelp-wblexemeeditformelements-param-formId":
"{{doc-apihelp-param|wblexemeeditformelements|formId}}",
+ "apihelp-wblexemeeditformelements-param-data":
"{{doc-apihelp-param|wblexemeeditformelements|data}}",
+ "apihelp-wblexemeeditformelements-param-bot":
"{{doc-apihelp-param|wblexemeeditformelements|bot}}",
+ "apihelp-wblexemeeditformelements-example-1":
"{{doc-apihelp-example|wblexemeeditformelements}}",
"wikibase-lexeme-api-error-parameter-required": "API error message that
is being reported when parameter is missing.",
"wikibase-lexeme-api-error-parameter-invalid-json-object": "API error
message that is being reported when provided data parameter is not a valid
JSON",
"wikibase-lexeme-api-error-lexeme-not-found": "API error message that
is being reported when Lexeme with provided ID does not
exist.\n\nParameters:\n* $1 - ID of the lexeme",
+ "wikibaselexeme-api-error-form-not-found": "API error message that is
being reported when Form with provided ID does not exist.\n\nParameters:\n* $1
- ID of the form",
"wikibase-lexeme-api-error-json-field-required": "API error message
that is being reported when required field in JSON object is not present.
\n\nParameters:\n* $1 - Name of the parameter that is expected to be JSON
object. \n* $2 - Path of the field inside of the JSON object (e.g
\"array-field-name/0/other-field-name\")",
"wikibase-lexeme-api-error-json-field-not-item-id": "API error message
that is being reported when field in JSON object expected to contain serialized
Item ID, but contains something else. \n\nParameters:\n* $1 - Name of the
parameter that is expected to be JSON object.\n* $2 - Path of the field inside
of the JSON object (e.g \"array-field-name/0/other-field-name\")\n* $3 -
Provided value",
"wikibase-lexeme-api-error-json-field-has-wrong-type": "API error
message that is being reported when field in JSON object expected to be of
certain type, but value of another type was provided. \n\nParameters:\n* $1 -
Name of the parameter that is expected to be JSON object.\n* $2 - Path of the
field inside of the JSON object (e.g
\"array-field-name/0/other-field-name\")\n* $3 - Expected type name (e.g.
string, array, object) \n* $4 - Provided value type name",
"wikibase-lexeme-api-error-parameter-not-lexeme-id": "API error message
that is being reported when parameter is expected to contain serialized Lexeme
ID, but contains something else. \n\nParameters:\n* $1 - Name of the
parameter\n* $2 - Provided value",
+ "wikibaselexeme-api-error-parameter-not-form-id": "API error message
that is being reported when parameter is expected to contain serialized Form
ID, but contains something else. \n\nParameters:\n* $1 - Name of the
parameter\n* $2 - Provided value",
"wikibase-lexeme-api-error-representation-text-cannot-be-empty": "API
error message that is being reported when one of provided representations is
empty",
"wikibase-lexeme-api-error-representation-language-cannot-be-empty":
"API error message that is being reported when one of provided representations'
language is empty",
"wikibase-lexeme-api-error-form-must-have-at-least-one-representation":
"API error message that is being reported when no representations were
provided",
diff --git a/src/Api/EditFormElements.php b/src/Api/EditFormElements.php
new file mode 100644
index 0000000..5eb2068
--- /dev/null
+++ b/src/Api/EditFormElements.php
@@ -0,0 +1,256 @@
+<?php
+
+namespace Wikibase\Lexeme\Api;
+
+use ApiMain;
+use Wikibase\EditEntityFactory;
+use Wikibase\Lexeme\Api\Error\FormNotFound;
+use Wikibase\Lexeme\DataModel\Form;
+use Wikibase\Lexeme\DataModel\Serialization\FormSerializer;
+use Wikibase\Lib\Store\EntityRevisionLookup;
+use Wikibase\Repo\WikibaseRepo;
+use Wikibase\SummaryFormatter;
+
+/**
+ * @license GPL-2.0+
+ */
+class EditFormElements extends \ApiBase {
+
+ /**
+ * @var EntityRevisionLookup
+ */
+ private $entityRevisionLookup;
+
+ /**
+ * @var EditEntityFactory
+ */
+ private $editEntityFactory;
+
+ /**
+ * @var EditFormElementsRequestParser
+ */
+ private $requestParser;
+
+ /**
+ * @var SummaryFormatter
+ */
+ private $summaryFormatter;
+
+ /**
+ * @var FormSerializer
+ */
+ private $formSerializer;
+
+ public static function newFromGlobalState( ApiMain $mainModule,
$moduleName ) {
+ $wikibaseRepo = WikibaseRepo::getDefaultInstance();
+
+ $serializerFactory =
$wikibaseRepo->getBaseDataModelSerializerFactory();
+
+ $formSerializer = new FormSerializer(
+ $serializerFactory->newTermListSerializer(),
+ $serializerFactory->newStatementListSerializer()
+ );
+
+ return new self(
+ $mainModule,
+ $moduleName,
+ $wikibaseRepo->getEntityRevisionLookup( 'uncached' ),
+ $wikibaseRepo->newEditEntityFactory(
$mainModule->getContext() ),
+ new EditFormElementsRequestParser(
$wikibaseRepo->getEntityIdParser() ),
+ $wikibaseRepo->getSummaryFormatter(),
+ $formSerializer
+ );
+ }
+
+ public function __construct(
+ ApiMain $mainModule,
+ $moduleName,
+ EntityRevisionLookup $entityRevisionLookup,
+ EditEntityFactory $editEntityFactory,
+ EditFormElementsRequestParser $requestParser,
+ SummaryFormatter $summaryFormatter,
+ FormSerializer $formSerializer
+ ) {
+ parent::__construct( $mainModule, $moduleName );
+
+ $this->entityRevisionLookup = $entityRevisionLookup;
+ $this->editEntityFactory = $editEntityFactory;
+ $this->requestParser = $requestParser;
+ $this->summaryFormatter = $summaryFormatter;
+ $this->formSerializer = $formSerializer;
+ }
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+ $parserResult = $this->requestParser->parse( $params );
+
+ if ( $parserResult->hasErrors() ) {
+ //TODO: Increase stats counter on failure
+ // `wikibase.repo.api.errors.total` counter
+ $this->dieStatus( $parserResult->asFatalStatus() );
+ }
+
+ $request = $parserResult->getRequest();
+ $formId = $request->getFormId();
+
+ $latestRevision = 0;
+ $formRevision = $this->entityRevisionLookup->getEntityRevision(
+ $formId,
+ $latestRevision,
+ EntityRevisionLookup::LATEST_FROM_MASTER
+ );
+
+ if ( $formRevision === null ) {
+ $error = new FormNotFound( $formId );
+ $this->dieWithError( $error->asApiMessage() );
+ }
+ $form = $formRevision->getEntity();
+
+ $changeOp = $request->getChangeOp();
+ $summary = new EditFormElementsSummary();
+ // TODO: We can not use summary in the changeop, we should
+ $changeOp->apply( $form );
+
+ $status = $this->saveForm( $form, $summary,
$formRevision->getRevisionId(), $params );
+
+ if ( !$status->isGood() ) {
+ $this->dieStatus( $status );
+ }
+
+ $this->generateResponse( $form );
+ }
+
+ /**
+ * @param Form $form
+ * @param EditFormElementsSummary $summary
+ * @param int $baseRevisionId
+ * @param array $params
+ * @return \Status
+ */
+ private function saveForm(
+ Form $form,
+ EditFormElementsSummary $summary,
+ $baseRevisionId,
+ array $params
+ ) {
+ $editEntity = $this->editEntityFactory->newEditEntity(
+ $this->getUser(),
+ $form->getId(),
+ $baseRevisionId
+ );
+
+ // TODO: bot flag should probably be part of the request
+ $flags = EDIT_UPDATE;
+ if ( isset( $params['bot'] ) && $params['bot'] &&
$this->getUser()->isAllowed( 'bot' ) ) {
+ $flags |= EDIT_FORCE_BOT;
+ }
+
+ $tokenThatDoesNotNeedChecking = false;
+ return $editEntity->attemptSave(
+ $form,
+ $this->summaryFormatter->formatSummary( $summary ),
+ $flags,
+ $tokenThatDoesNotNeedChecking
+ );
+ }
+
+ /**
+ * @param Form $form
+ */
+ private function generateResponse( Form $form ) {
+ $apiResult = $this->getResult();
+
+ $serializedForm = $this->formSerializer->serialize( $form );
+ unset( $serializedForm['claims'] );
+
+ // TODO: Do we really need `success` property in response?
+ $apiResult->addValue( null, 'success', 1 );
+ $apiResult->addValue( null, 'form', $serializedForm );
+ }
+
+ /**
+ * @see ApiBase::getAllowedParams
+ */
+ protected function getAllowedParams() {
+ return [
+ 'formId' => [
+ self::PARAM_TYPE => 'string',
+ self::PARAM_REQUIRED => true,
+ ],
+ 'data' => [
+ self::PARAM_TYPE => 'text',
+ self::PARAM_REQUIRED => true,
+ ],
+ 'bot' => [
+ self::PARAM_TYPE => 'boolean',
+ self::PARAM_DFLT => false,
+ ]
+ ];
+ }
+
+ /**
+ * @see ApiBase::isWriteMode()
+ */
+ public function isWriteMode() {
+ return true;
+ }
+
+ /**
+ * @see ApiBase::needsToken()
+ */
+ public function needsToken() {
+ return 'csrf';
+ }
+
+ /**
+ * @see ApiBase::mustBePosted()
+ */
+ public function mustBePosted() {
+ return true;
+ }
+
+ protected function getExamplesMessages() {
+ $formId = 'L12';
+ $exampleData = [
+ 'representations' => [
+ [ 'representation' => 'color', 'language' =>
'en-US' ],
+ [ 'representation' => 'colour', 'language' =>
'en-GB' ],
+ ],
+ 'grammaticalFeatures' => [
+ 'Q1', 'Q2'
+ ]
+ ];
+
+ $query = http_build_query( [
+ 'action' => $this->getModuleName(),
+ 'formId' => $formId,
+ 'data' => json_encode( $exampleData )
+ ] );
+
+ $languages = array_map( function ( $r ) {
+ return $r['language'];
+ }, $exampleData['representations'] );
+ $representations = array_map( function ( $r ) {
+ return $r['representation'];
+ }, $exampleData['representations'] );
+
+ $representationsText = $this->getLanguage()->commaList(
$representations );
+ $languagesText = $this->getLanguage()->commaList( $languages );
+ $grammaticalFeaturesText = $this->getLanguage()->commaList(
$exampleData['grammaticalFeatures'] );
+
+ $exampleMessage = new \Message(
+ 'apihelp-wblexemeeditformelements-example-1',
+ [
+ $formId,
+ $representationsText,
+ $languagesText,
+ $grammaticalFeaturesText
+ ]
+ );
+
+ return [
+ $query => $exampleMessage
+ ];
+ }
+
+}
diff --git a/src/Api/EditFormElementsRequest.php
b/src/Api/EditFormElementsRequest.php
new file mode 100644
index 0000000..ffdbcd9
--- /dev/null
+++ b/src/Api/EditFormElementsRequest.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Wikibase\Lexeme\Api;
+
+use Wikibase\DataModel\Entity\ItemId;
+use Wikibase\DataModel\Term\TermList;
+use Wikibase\Lexeme\ChangeOp\ChangeOpEditFormElements;
+use Wikibase\Lexeme\DataModel\FormId;
+
+/**
+ * @license GPL-2.0+
+ */
+class EditFormElementsRequest {
+
+ /**
+ * @var FormId
+ */
+ private $formId;
+
+ /**
+ * @var TermList
+ */
+ private $representations;
+
+ /**
+ * @var ItemId[]
+ */
+ private $grammaticalFeatures;
+
+ public function __construct(
+ FormId $formId,
+ TermList $representations,
+ array $grammaticalFeatures
+ ) {
+ $this->formId = $formId;
+ $this->representations = $representations;
+ $this->grammaticalFeatures = $grammaticalFeatures;
+ }
+
+ public function getChangeOp() {
+ return new ChangeOpEditFormElements( $this->representations,
$this->grammaticalFeatures );
+ }
+
+ public function getFormId() {
+ return $this->formId;
+ }
+
+}
diff --git a/src/Api/EditFormElementsRequestParser.php
b/src/Api/EditFormElementsRequestParser.php
new file mode 100644
index 0000000..2a6c1cd
--- /dev/null
+++ b/src/Api/EditFormElementsRequestParser.php
@@ -0,0 +1,243 @@
+<?php
+
+namespace Wikibase\Lexeme\Api;
+
+use Wikibase\DataModel\Entity\EntityIdParser;
+use Wikibase\DataModel\Entity\EntityIdParsingException;
+use Wikibase\DataModel\Entity\ItemId;
+use Wikibase\DataModel\Entity\ItemIdParser;
+use Wikibase\DataModel\Term\Term;
+use Wikibase\DataModel\Term\TermList;
+use Wikibase\Lexeme\Api\Error\ApiError;
+use Wikibase\Lexeme\Api\Error\FormMustHaveAtLeastOneRepresentation;
+use Wikibase\Lexeme\Api\Error\JsonFieldHasWrongType;
+use Wikibase\Lexeme\Api\Error\JsonFieldIsNotAnItemId;
+use Wikibase\Lexeme\Api\Error\JsonFieldIsRequired;
+use Wikibase\Lexeme\Api\Error\ParameterIsNotAJsonObject;
+use Wikibase\Lexeme\Api\Error\ParameterIsNotFormId;
+use Wikibase\Lexeme\Api\Error\ParameterIsRequired;
+use Wikibase\Lexeme\Api\Error\RepresentationLanguageCanNotBeEmpty;
+use Wikibase\Lexeme\Api\Error\RepresentationsMustHaveUniqueLanguage;
+use Wikibase\Lexeme\Api\Error\RepresentationTextCanNotBeEmpty;
+use Wikibase\Lexeme\DataModel\FormId;
+
+/**
+ * @license GPL-2.0+
+ */
+class EditFormElementsRequestParser {
+
+ const PARAM_DATA = 'data';
+
+ const PARAM_FORM_ID = 'formId';
+
+ /**
+ * @var ItemIdParser
+ */
+ private $itemIdParser;
+
+ /**
+ * @var EntityIdParser
+ */
+ private $entityIdParser;
+
+ public function __construct( EntityIdParser $entityIdParser ) {
+ $this->itemIdParser = new ItemIdParser();
+ $this->entityIdParser = $entityIdParser;
+ }
+
+ public function parse( array $params ) {
+ //TODO: validate language. How?
+ //TODO: validate if all grammatical features exist
+ $errors = $this->validateRequiredFieldsPresent( $params );
+ if ( $errors ) {
+ return
EditFormElementsRequestParserResult::newWithErrors( $errors );
+ }
+
+ $data = json_decode( $params[self::PARAM_DATA] );
+ if ( !is_object( $data ) ) {
+ return
EditFormElementsRequestParserResult::newWithErrors(
+ [
+ new ParameterIsNotAJsonObject(
self::PARAM_DATA, $params[self::PARAM_DATA] )
+ ]
+ );
+ }
+
+ $errors = $this->validateDataStructure( $data );
+ if ( $errors ) {
+ return
EditFormElementsRequestParserResult::newWithErrors( $errors );
+ }
+
+ $formId = $this->parseFormId( $params['formId'], $errors );
+ $representations = $this->parseRepresentations(
$data->representations, $errors );
+ $grammaticalFeatures = $this->parseGrammaticalFeatures(
$data->grammaticalFeatures, $errors );
+
+ if ( $errors ) {
+ return
EditFormElementsRequestParserResult::newWithErrors( $errors );
+ }
+
+ return EditFormElementsRequestParserResult::newWithRequest(
+ new EditFormElementsRequest(
+ $formId,
+ $representations,
+ $grammaticalFeatures
+ )
+ );
+ }
+
+ /**
+ * @param $id
+ * @param array &$errors
+ * @return FormId|null
+ */
+ private function parseFormId( $id, array &$errors ) {
+ try {
+ $formId = $this->entityIdParser->parse( $id );
+ } catch ( EntityIdParsingException $e ) {
+ $errors[] = new ParameterIsNotFormId(
self::PARAM_FORM_ID, $id );
+ return null;
+ }
+
+ if ( $formId->getEntityType() !== 'form' ) {
+ $errors[] = new ParameterIsNotFormId(
self::PARAM_FORM_ID, $id );
+ return null;
+ }
+
+ return $formId;
+ }
+
+ /**
+ * @param \stdClass[] $givenRepresentations
+ * @param ApiError[] &$errors
+ * @return TermList
+ */
+ private function parseRepresentations( array $givenRepresentations,
array &$errors ) {
+ if ( empty( $givenRepresentations ) ) {
+ $errors[] = new FormMustHaveAtLeastOneRepresentation(
self::PARAM_DATA, [ 'representations' ] );
+ }
+
+ //FIXME: Array may contain representation with empty text (or
untrimmed) which won't be added
+ $result = [];
+
+ foreach ( $givenRepresentations as $index => $el ) {
+ $incomplete = false;
+
+ if ( !property_exists( $el, 'representation' ) ) {
+ $errors[] = new JsonFieldIsRequired(
+ self::PARAM_DATA,
+ [ 'representations', $index,
'representation' ]
+ );
+ $incomplete = true;
+ } elseif ( empty( $el->representation ) ) {
+ $errors[] = new RepresentationTextCanNotBeEmpty(
+ self::PARAM_DATA,
+ [ 'representations', $index,
'representation' ]
+ );
+ $incomplete = true;
+ }
+
+ if ( !property_exists( $el, 'language' ) ) {
+ $errors[] = new JsonFieldIsRequired(
+ self::PARAM_DATA,
+ [ 'representations', $index, 'language'
]
+ );
+ $incomplete = true;
+ } elseif ( empty( $el->language ) ) {
+ $errors[] = new
RepresentationLanguageCanNotBeEmpty(
+ self::PARAM_DATA,
+ [ 'representations', $index, 'language'
]
+ );
+ $incomplete = true;
+ }
+
+ if ( $incomplete ) {
+ continue;
+ }
+
+ if ( isset( $result[$el->language] ) ) {
+ $errors[] = new
RepresentationsMustHaveUniqueLanguage(
+ self::PARAM_DATA,
+ [ 'representations', $index, 'language'
],
+ $el->language
+ );
+ }
+
+ $result[$el->language] = $el->representation;
+ }
+
+ $terms = [];
+ foreach ( $result as $language => $representation ) {
+ $terms[] = new Term( $language, $representation );
+ }
+
+ return new TermList( $terms );
+ }
+
+ /**
+ * @param string[] $data
+ * @param ApiError[] $errors
+ * @return ItemId[]
+ */
+ private function parseGrammaticalFeatures( array $data, array &$errors
) {
+ $features = [];
+
+ foreach ( $data as $index => $featureId ) {
+ try {
+ $id = $this->itemIdParser->parse( $featureId );
+ } catch ( EntityIdParsingException $e ) {
+ $errors[] = new JsonFieldIsNotAnItemId(
+ self::PARAM_DATA,
+ [ 'grammaticalFeatures', $index ],
+ $featureId
+ );
+ continue;
+ }
+
+ $features[] = $id;
+ }
+
+ return $features;
+ }
+
+ private function validateDataStructure( $data ) {
+ $errors = [];
+
+ if ( !property_exists( $data, 'representations' ) ) {
+ $errors[] = new JsonFieldIsRequired( self::PARAM_DATA,
[ 'representations' ] );
+ } elseif ( !is_array( $data->representations ) ) {
+ $errors[] = new JsonFieldHasWrongType(
+ self::PARAM_DATA,
+ [ 'representations' ],
+ 'array',
+ gettype( $data->representations )
+ );
+ }
+
+ if ( !property_exists( $data, 'grammaticalFeatures' ) ) {
+ $errors[] = new JsonFieldIsRequired( self::PARAM_DATA,
[ 'grammaticalFeatures' ] );
+ } elseif ( !is_array( $data->grammaticalFeatures ) ) {
+ $errors[] = new JsonFieldHasWrongType(
+ self::PARAM_DATA,
+ [ 'grammaticalFeatures' ],
+ 'array',
+ gettype( $data->grammaticalFeatures )
+ );
+ }
+
+ return $errors;
+ }
+
+ private function validateRequiredFieldsPresent( array $params ) {
+ $errors = [];
+
+ if ( !array_key_exists( self::PARAM_FORM_ID, $params ) ) {
+ $errors[] = new ParameterIsRequired(
self::PARAM_FORM_ID );
+ }
+
+ if ( !array_key_exists( self::PARAM_DATA, $params ) ) {
+ $errors[] = new ParameterIsRequired( self::PARAM_DATA );
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/Api/EditFormElementsRequestParserResult.php
b/src/Api/EditFormElementsRequestParserResult.php
new file mode 100644
index 0000000..f9ff9ea
--- /dev/null
+++ b/src/Api/EditFormElementsRequestParserResult.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Wikibase\Lexeme\Api;
+
+use Wikibase\Lexeme\Api\Error\ApiError;
+use Wikimedia\Assert\Assert;
+
+/**
+ * @license GPL-2.0+
+ */
+class EditFormElementsRequestParserResult {
+
+ /**
+ * @var EditFormElementsRequest|null
+ */
+ private $request;
+
+ /**
+ * @var ApiError[]
+ */
+ private $errors;
+
+ public static function newWithRequest( EditFormElementsRequest $request
) {
+ return new self( $request, [] );
+ }
+
+ /**
+ * @param ApiError[] $errors
+ * @return EditFormElementsRequestParserResult
+ */
+ public static function newWithErrors( array $errors ) {
+ return new self( null, $errors );
+ }
+
+ /**
+ * @param EditFormElementsRequest|null $request
+ * @param ApiError[] $errors
+ */
+ private function __construct( EditFormElementsRequest $request = null,
array $errors ) {
+ Assert::parameterElementType( ApiError::class, $errors,
'$errors' );
+ $this->request = $request;
+ $this->errors = $errors;
+ }
+
+ /**
+ * @return EditFormElementsRequest
+ */
+ public function getRequest() {
+ if ( $this->errors ) {
+ throw new \LogicException(
+ 'There have been errors when parsing the
request. Call getErrors to handle them'
+ );
+ }
+
+ return $this->request;
+ }
+
+ public function hasErrors() {
+ return !empty( $this->errors );
+ }
+
+ /**
+ * @return \Status
+ */
+ public function asFatalStatus() {
+ if ( !$this->hasErrors() ) {
+ throw new \LogicException( 'Succesful result can not be
converted to fatal status' );
+ }
+
+ $status = \Status::newGood();
+ foreach ( $this->errors as $error ) {
+ $status->fatal( $error->asApiMessage() );
+ }
+ return $status;
+ }
+
+}
diff --git a/src/Api/EditFormElementsSummary.php
b/src/Api/EditFormElementsSummary.php
new file mode 100644
index 0000000..134d886
--- /dev/null
+++ b/src/Api/EditFormElementsSummary.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Wikibase\Lexeme\Api;
+
+use Wikibase\Lib\FormatableSummary;
+
+/**
+ * @license GPL-2.0+
+ */
+class EditFormElementsSummary implements FormatableSummary {
+
+ public function getUserSummary() {
+ return null;
+ }
+
+ public function getLanguageCode() {
+ return null;
+ }
+
+ public function getMessageKey() {
+ /** @see "wikibase-lexeme-summary-set-form" message */
+ return 'set-form';
+ }
+
+ public function getCommentArgs() {
+ return [];
+ }
+
+ public function getAutoSummaryArgs() {
+ return [];
+ }
+
+}
diff --git a/src/Api/Error/FormNotFound.php b/src/Api/Error/FormNotFound.php
new file mode 100644
index 0000000..b3363ab
--- /dev/null
+++ b/src/Api/Error/FormNotFound.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Wikibase\Lexeme\Api\Error;
+
+use ApiMessage;
+use Message;
+use Wikibase\Lexeme\DataModel\FormId;
+
+/**
+ * @license GPL-2.0+
+ */
+class FormNotFound implements ApiError {
+
+ /**
+ * @var FormId
+ */
+ private $formId;
+
+ public function __construct( FormId $formId ) {
+ $this->formId = $formId;
+ }
+
+ /**
+ * @return ApiMessage
+ */
+ public function asApiMessage() {
+ $message = new Message(
+ 'wikibaselexeme-api-error-form-not-found',
+ [ $this->formId->serialize() ]
+ );
+ return new ApiMessage( $message, 'not-found' );
+ }
+
+}
diff --git a/src/Api/Error/ParameterIsNotFormId.php
b/src/Api/Error/ParameterIsNotFormId.php
new file mode 100644
index 0000000..182e6b2
--- /dev/null
+++ b/src/Api/Error/ParameterIsNotFormId.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Wikibase\Lexeme\Api\Error;
+
+use ApiMessage;
+
+/**
+ * @license GPL-2.0+
+ */
+class ParameterIsNotFormId implements ApiError {
+
+ /**
+ * @var string
+ */
+ private $parameterName;
+
+ /**
+ * @var string
+ */
+ private $given;
+
+ /**
+ * @param string $parameterName
+ * @param string $given
+ */
+ public function __construct( $parameterName, $given ) {
+ $this->parameterName = $parameterName;
+ $this->given = $given;
+ }
+
+ /**
+ * @see ApiError::asApiMessage()
+ */
+ public function asApiMessage() {
+ // Parameter "$1" expected to be a valid Form ID (ex.
"L10-F1"), given "$2"
+ $message = new \Message(
+ 'wikibaselexeme-api-error-parameter-not-form-id',
+ [ $this->parameterName, $this->given ]
+ );
+ return new ApiMessage( $message, 'bad-request' );
+ }
+
+}
diff --git a/src/ChangeOp/ChangeOpEditFormElements.php
b/src/ChangeOp/ChangeOpEditFormElements.php
new file mode 100644
index 0000000..b31ed8a
--- /dev/null
+++ b/src/ChangeOp/ChangeOpEditFormElements.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Wikibase\Lexeme\ChangeOp;
+
+use ValueValidators\Result;
+use Wikibase\DataModel\Entity\EntityDocument;
+use Wikibase\DataModel\Entity\ItemId;
+use Wikibase\DataModel\Term\TermList;
+use Wikibase\Lexeme\DataModel\Form;
+use Wikibase\Repo\ChangeOp\ChangeOp;
+use Wikibase\Repo\Store\EntityPermissionChecker;
+use Wikibase\Summary;
+use Wikimedia\Assert\Assert;
+
+/**
+ * @license GPL-2.0+
+ */
+class ChangeOpEditFormElements implements ChangeOp {
+
+ /**
+ * @var TermList
+ */
+ private $representations;
+
+ /**
+ * @var ItemId[]
+ */
+ private $grammaticalFeatures;
+
+ public function __construct( TermList $representations, array
$grammaticalFeatures ) {
+ $this->representations = $representations;
+ $this->grammaticalFeatures = $grammaticalFeatures;
+ }
+
+ public function validate( EntityDocument $entity ) {
+ // TODO: Should this be also a change op applicable on Lexeme
entities
+ // (e.g. when used in wbeditentity)?
+ Assert::parameterType( Form::class, $entity, '$entity' );
+
+ return Result::newSuccess();
+ }
+
+ public function apply( EntityDocument $entity, Summary $summary = null
) {
+ // TODO: Should this be also a change op applicable on Lexeme
entities
+ // (e.g. when used in wbeditentity)?
+ Assert::parameterType( Form::class, $entity, '$entity' );
+
+ /** @var Form $entity */
+ $entity->setRepresentations( $this->representations );
+ $entity->setGrammaticalFeatures( $this->grammaticalFeatures );
+ }
+
+ public function getActions() {
+ return [ EntityPermissionChecker::ACTION_EDIT ];
+ }
+
+}
diff --git a/src/DataModel/Form.php b/src/DataModel/Form.php
index b9b5879..efc4b44 100644
--- a/src/DataModel/Form.php
+++ b/src/DataModel/Form.php
@@ -101,6 +101,10 @@
return $this->representations;
}
+ public function setRepresentations( TermList $representations ) {
+ $this->representations = $representations;
+ }
+
/**
* @return ItemId[]
*/
diff --git a/tests/phpunit/mediawiki/Api/EditFormElementsTest.php
b/tests/phpunit/mediawiki/Api/EditFormElementsTest.php
new file mode 100644
index 0000000..b6e3fb7
--- /dev/null
+++ b/tests/phpunit/mediawiki/Api/EditFormElementsTest.php
@@ -0,0 +1,375 @@
+<?php
+
+namespace Wikibase\Lexeme\Tests\MediaWiki\Api;
+
+use ApiMessage;
+use ApiUsageException;
+use Wikibase\DataModel\Entity\ItemId;
+use Wikibase\Lexeme\DataModel\FormId;
+use Wikibase\Lexeme\DataModel\Lexeme;
+use Wikibase\Lexeme\DataModel\LexemeId;
+use Wikibase\Lexeme\Tests\DataModel\NewForm;
+use Wikibase\Lexeme\Tests\DataModel\NewLexeme;
+use Wikibase\Repo\Tests\Api\WikibaseApiTestCase;
+use Wikibase\Repo\WikibaseRepo;
+
+/**
+ * @covers \Wikibase\Lexeme\Api\EditFormElements
+ *
+ * @license GPL-2.0+
+ *
+ * @group Database
+ * @group medium
+ */
+class EditFormElementsTest extends WikibaseApiTestCase {
+
+ /**
+ * @dataProvider provideInvalidParams
+ */
+ public function testGivenInvalidParameter_errorIsReturned(
+ array $params,
+ array $expectedError
+ ) {
+ $this->setContentLang( 'qqq' );
+ $params = array_merge(
+ [ 'action' => 'wblexemeeditformelements' ],
+ $params
+ );
+
+ try {
+ $this->doApiRequestWithToken( $params );
+ $this->fail( 'No API error was raised' );
+ } catch ( ApiUsageException $e ) {
+ /** @var ApiMessage $message */
+ $message = $e->getMessageObject();
+
+ $this->assertInstanceOf( ApiMessage::class, $message );
+ $this->assertEquals( $expectedError['message-key'],
$message->getKey(), 'Wrong message codes' );
+ $this->assertEquals(
+ $expectedError['message-parameters'],
+ $message->getParams(),
+ 'Wrong message parameters'
+ );
+ $this->assertEquals(
+ $expectedError['api-error-code'],
+ $message->getApiCode(),
+ 'Wrong api code'
+ );
+ $this->assertEquals(
+ $expectedError['api-error-data'],
+ $message->getApiData(),
+ 'Wrong api data'
+ );
+ }
+ }
+
+ private function getDataParam( array $dataToUse = [] ) {
+ $simpleData = [
+ 'representations' => [
+ [
+ 'language' => 'en',
+ 'representation' => 'colour'
+ ]
+ ],
+ 'grammaticalFeatures' => [ 'Q17' ],
+ ];
+
+ return json_encode( array_merge( $simpleData, $dataToUse ) );
+ }
+
+ public function provideInvalidParams() {
+ return [
+ 'no formId param' => [
+ [ 'data' => $this->getDataParam() ],
+ [
+ 'message-key' =>
'apierror-missingparam',
+ 'message-parameters' => [ 'formId' ],
+ 'api-error-code' => 'noformId',
+ 'api-error-data' => []
+ ],
+ ],
+ 'no data param' => [
+ [ 'formId' => 'L1-F1' ],
+ [
+ 'message-key' =>
'apierror-missingparam',
+ 'message-parameters' => [ 'data' ],
+ 'api-error-code' => 'nodata',
+ 'api-error-data' => []
+ ],
+ ],
+ 'invalid form ID (random string not ID)' => [
+ [ 'formId' => 'foo', 'data' =>
$this->getDataParam() ],
+ [
+ 'message-key' =>
'wikibaselexeme-api-error-parameter-not-form-id',
+ 'message-parameters' => [ 'formId',
'foo' ],
+ 'api-error-code' => 'bad-request',
+ 'api-error-data' => []
+ ]
+ ],
+ 'data not a well-formed JSON object' => [
+ [ 'formId' => 'L1-F1', 'data' => '{foo' ],
+ [
+ 'message-key' =>
'wikibase-lexeme-api-error-parameter-invalid-json-object',
+ 'message-parameters' => [ 'data',
'{foo' ],
+ 'api-error-code' => 'bad-request',
+ 'api-error-data' => []
+ ],
+ ],
+ 'Form is not found' => [
+ [ 'formId' => 'L999-F1', 'data' =>
$this->getDataParam() ],
+ [
+ 'message-key' =>
'wikibaselexeme-api-error-form-not-found',
+ 'message-parameters' => [ 'L999-F1' ],
+ 'api-error-code' => 'not-found',
+ 'api-error-data' => []
+ ],
+ ],
+ ];
+ }
+
+ public function
testGivenOtherRepresentations_changesRepresentationsOfForm() {
+ $form = NewForm::havingId( 'F1' )->andRepresentation( 'en',
'goat' )->build();
+ $lexeme = NewLexeme::havingId( 'L1' )->withForm( $form
)->build();
+
+ $this->saveLexeme( $lexeme );
+
+ $params = [
+ 'action' => 'wblexemeeditformelements',
+ 'formId' => 'L1-F1',
+ 'data' => json_encode( [
+ 'representations' => [
+ [ 'language' => 'en', 'representation'
=> 'goadth' ],
+ ],
+ 'grammaticalFeatures' => [],
+ ] ),
+ ];
+
+ $this->doApiRequestWithToken( $params );
+
+ $lexeme = $this->getLexeme( 'L1' );
+
+ $form = $lexeme->getForms()->getById( new FormId( 'L1-F1' ) );
+ $this->assertEquals( 'goadth',
$form->getRepresentations()->getByLanguage( 'en' )->getText() );
+ }
+
+ public function
testGivenRepresentationNotThere_representationIsRemoved() {
+ $form = NewForm::havingId( 'F1' )
+ ->andRepresentation( 'en', 'colour' )
+ ->andRepresentation( 'en-us', 'color' )
+ ->build();
+ $lexeme = NewLexeme::havingId( 'L1' )->withForm( $form
)->build();
+
+ $this->saveLexeme( $lexeme );
+
+ $params = [
+ 'action' => 'wblexemeeditformelements',
+ 'formId' => 'L1-F1',
+ 'data' => json_encode( [
+ 'representations' => [
+ [ 'language' => 'en', 'representation'
=> 'colour' ],
+ ],
+ 'grammaticalFeatures' => [],
+ ] ),
+ ];
+
+ $this->doApiRequestWithToken( $params );
+
+ $lexeme = $this->getLexeme( 'L1' );
+
+ $form = $lexeme->getForms()->getById( new FormId( 'L1-F1' ) );
+ $this->assertEquals( 'colour',
$form->getRepresentations()->getByLanguage( 'en' )->getText() );
+ $this->assertFalse(
$form->getRepresentations()->hasTermForLanguage( 'en-us' ) );
+ }
+
+ public function
testGivenRepresentationForNewLanguage_representationIsAdded() {
+ $form = NewForm::havingId( 'F1' )
+ ->andRepresentation( 'en', 'colour' )
+ ->build();
+ $lexeme = NewLexeme::havingId( 'L1' )->withForm( $form
)->build();
+
+ $this->saveLexeme( $lexeme );
+
+ $params = [
+ 'action' => 'wblexemeeditformelements',
+ 'formId' => 'L1-F1',
+ 'data' => json_encode( [
+ 'representations' => [
+ [ 'language' => 'en', 'representation'
=> 'colour' ],
+ [ 'language' => 'en-us',
'representation' => 'color' ],
+ ],
+ 'grammaticalFeatures' => [],
+ ] ),
+ ];
+
+ $this->doApiRequestWithToken( $params );
+
+ $lexeme = $this->getLexeme( 'L1' );
+
+ $form = $lexeme->getForms()->getById( new FormId( 'L1-F1' ) );
+ $this->assertEquals( 'colour',
$form->getRepresentations()->getByLanguage( 'en' )->getText() );
+ $this->assertEquals( 'color',
$form->getRepresentations()->getByLanguage( 'en-us' )->getText() );
+ }
+
+ public function
testGivenOtherGrammaticalFeatures_grammaticalFeaturesAreChanged() {
+ $form = NewForm::havingId( 'F1' )
+ ->andGrammaticalFeature( 'Q123' )
+ ->andRepresentation( 'en', 'goat' )
+ ->build();
+ $lexeme = NewLexeme::havingId( 'L1' )->withForm( $form
)->build();
+
+ $this->saveLexeme( $lexeme );
+
+ $params = [
+ 'action' => 'wblexemeeditformelements',
+ 'formId' => 'L1-F1',
+ 'data' => json_encode( [
+ 'representations' => [
+ [ 'language' => 'en', 'representation'
=> 'goat' ],
+ ],
+ 'grammaticalFeatures' => [ 'Q321' ],
+ ] ),
+ ];
+
+ $this->doApiRequestWithToken( $params );
+
+ $lexeme = $this->getLexeme( 'L1' );
+
+ $form = $lexeme->getForms()->getById( new FormId( 'L1-F1' ) );
+ $this->assertEquals( [ new ItemId( 'Q321' ) ],
$form->getGrammaticalFeatures() );
+ }
+
+ public function
testGivenNewGrammaticalFeature_grammaticalFeatureIsAdded() {
+ $form = NewForm::havingId( 'F1' )
+ ->andGrammaticalFeature( 'Q123' )
+ ->andRepresentation( 'en', 'goat' )
+ ->build();
+ $lexeme = NewLexeme::havingId( 'L1' )->withForm( $form
)->build();
+
+ $this->saveLexeme( $lexeme );
+
+ $params = [
+ 'action' => 'wblexemeeditformelements',
+ 'formId' => 'L1-F1',
+ 'data' => json_encode( [
+ 'representations' => [
+ [ 'language' => 'en', 'representation'
=> 'goat' ],
+ ],
+ 'grammaticalFeatures' => [ 'Q123', 'Q678' ],
+ ] ),
+ ];
+
+ $this->doApiRequestWithToken( $params );
+
+ $lexeme = $this->getLexeme( 'L1' );
+
+ $form = $lexeme->getForms()->getById( new FormId( 'L1-F1' ) );
+ $this->assertEquals(
+ [ new ItemId( 'Q123' ), new ItemId( 'Q678' ) ],
+ $form->getGrammaticalFeatures()
+ );
+ }
+
+ public function
testGivenNoGrammaticalFeature_grammaticalFeatureIsRemoved() {
+ $form = NewForm::havingId( 'F1' )
+ ->andGrammaticalFeature( 'Q123' )
+ ->andRepresentation( 'en', 'goat' )
+ ->build();
+ $lexeme = NewLexeme::havingId( 'L1' )->withForm( $form
)->build();
+
+ $this->saveLexeme( $lexeme );
+
+ $params = [
+ 'action' => 'wblexemeeditformelements',
+ 'formId' => 'L1-F1',
+ 'data' => json_encode( [
+ 'representations' => [
+ [ 'language' => 'en', 'representation'
=> 'goat' ],
+ ],
+ 'grammaticalFeatures' => [],
+ ] ),
+ ];
+
+ $this->doApiRequestWithToken( $params );
+
+ $lexeme = $this->getLexeme( 'L1' );
+
+ $form = $lexeme->getForms()->getById( new FormId( 'L1-F1' ) );
+ $this->assertEmpty( $form->getGrammaticalFeatures() );
+ }
+
+ // TODO: test summary once its set!
+
+ public function testGivenFormEdited_responseContainsSuccessMarker() {
+ $form = NewForm::havingId( 'F1' )
+ ->andGrammaticalFeature( 'Q123' )
+ ->andRepresentation( 'en', 'goat' )
+ ->build();
+ $lexeme = NewLexeme::havingId( 'L1' )->withForm( $form
)->build();
+
+ $this->saveLexeme( $lexeme );
+
+ $params = [
+ 'action' => 'wblexemeeditformelements',
+ 'formId' => 'L1-F1',
+ 'data' => $this->getDataParam()
+ ];
+
+ list( $result, ) = $this->doApiRequestWithToken( $params );
+
+ $this->assertSame( 1, $result['success'] );
+ }
+
+ public function testGivenFormEdited_responseContainsSavedFormData() {
+ $form = NewForm::havingId( 'F1' )
+ ->andRepresentation( 'en', 'colour' )
+ ->build();
+ $lexeme = NewLexeme::havingId( 'L1' )->withForm( $form
)->build();
+
+ $this->saveLexeme( $lexeme );
+
+ $params = [
+ 'action' => 'wblexemeeditformelements',
+ 'formId' => 'L1-F1',
+ 'data' => json_encode( [
+ 'representations' => [
+ [ 'language' => 'en', 'representation'
=> 'colour' ],
+ [ 'language' => 'en-us',
'representation' => 'color' ],
+ ],
+ 'grammaticalFeatures' => [ 'Q321' ],
+ ] ),
+ ];
+
+ list( $result, ) = $this->doApiRequestWithToken( $params );
+
+ $this->assertEquals(
+ [
+ 'id' => 'L1-F1',
+ 'representations' => [
+ 'en' => [ 'language' => 'en', 'value'
=> 'colour' ],
+ 'en-us' => [ 'language' => 'en-us',
'value' => 'color' ],
+ ],
+ 'grammaticalFeatures' => [ 'Q321' ],
+ ],
+ $result['form']
+ );
+ }
+
+ // TODO: test API response contains the revision ID
+
+ private function saveLexeme( Lexeme $lexeme ) {
+ $store = WikibaseRepo::getDefaultInstance()->getEntityStore();
+
+ $store->saveEntity( $lexeme, self::class, $this->getMock(
\User::class ) );
+ }
+
+ /**
+ * @param string $id
+ *
+ * @return Lexeme|null
+ */
+ private function getLexeme( $id ) {
+ $lookup = WikibaseRepo::getDefaultInstance()->getEntityLookup();
+ return $lookup->getEntity( new LexemeId( $id ) );
+ }
+
+}
--
To view, visit https://gerrit.wikimedia.org/r/402868
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: Ice096f53a09b80aa26d2ced342a7986b82d01998
Gerrit-PatchSet: 11
Gerrit-Project: mediawiki/extensions/WikibaseLexeme
Gerrit-Branch: master
Gerrit-Owner: WMDE-leszek <[email protected]>
Gerrit-Reviewer: Jonas Kress (WMDE) <[email protected]>
Gerrit-Reviewer: Thiemo Kreuz (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