diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 439ee29..b24d4ad 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,4 +6,5 @@ include: ref: main file: - 'templates/groups/pkp_plugin.yml' - - 'templates/groups/ops/cypress_tests.yml' \ No newline at end of file + - 'templates/groups/ops/cypress_tests.yml' + - 'templates/groups/ops/unit_tests.yml' \ No newline at end of file diff --git a/ScieloTranslationsFieldsPlugin.php b/ScieloTranslationsFieldsPlugin.php index 965b55d..a0c4a48 100644 --- a/ScieloTranslationsFieldsPlugin.php +++ b/ScieloTranslationsFieldsPlugin.php @@ -18,7 +18,9 @@ use PKP\plugins\GenericPlugin; use APP\core\Application; use APP\pages\submission\SubmissionHandler; +use APP\plugins\generic\scieloTranslationsFields\api\v1\translationsFields\TranslationsFieldsHandler; use APP\plugins\generic\scieloTranslationsFields\classes\components\forms\TranslationDataForm; +use APP\plugins\generic\scieloTranslationsFields\classes\DoiValidator; class ScieloTranslationsFieldsPlugin extends GenericPlugin { @@ -36,6 +38,7 @@ public function register($category, $path, $mainContextId = null) Hook::add('Template::Workflow', [$this, 'removeRelationsFromWorkflow']); Hook::add('TemplateManager::display', [$this, 'addResourcesToWorkflow']); Hook::add('Template::Workflow::Publication', [$this, 'addTranslationDataTabToWorkflow']); + Hook::add('Dispatcher::dispatch', [$this, 'setupTranslationsFieldsHandler']); Hook::add('Schema::get::publication', [$this, 'addOurFieldsToPublicationSchema']); } @@ -66,6 +69,11 @@ public function addOurFieldsToPublicationSchema($hookName, $params) 'apiSummary' => true, 'validation' => ['nullable'], ]; + $schema->properties->{'originalDocumentCitation'} = (object) [ + 'type' => 'string', + 'apiSummary' => true, + 'validation' => ['nullable'], + ]; return Hook::CONTINUE; } @@ -120,19 +128,19 @@ private function removeRelationsSection($stepSections) return $editedSections; } - private function getTranslationDataForm($submission, $request) + private function getTranslationDataForm($submission, $request, $placedOn) { $context = $request->getContext(); - $publication = $submission->getLatestPublication(); - $publicationEndpoint = 'submissions/' . $submission->getId() . '/publications/' . $publication->getId(); - $saveFormUrl = $request->getDispatcher()->url($request, Application::ROUTE_API, $context->getPath(), $publicationEndpoint); + $saveFormUrl = $request + ->getDispatcher() + ->url($request, Application::ROUTE_API, $context->getPath(), "translationsFields/saveTranslationFields", null, null, ['submissionId' => $submission->getId()]); - return new TranslationDataForm($saveFormUrl, $submission); + return new TranslationDataForm($saveFormUrl, $submission, $placedOn); } private function addTranslationDataSection($stepSections, $submission, $request) { - $translationDataForm = $this->getTranslationDataForm($submission, $request); + $translationDataForm = $this->getTranslationDataForm($submission, $request, 'submissionWizard'); $stepSections[] = [ 'id' => 'translationData', @@ -174,10 +182,18 @@ public function validateSubmissionFields($hookName, $params) $submission = $params[1]; $publication = $submission->getCurrentPublication(); - if (is_null($publication->getData('originalDocumentHasDoi'))) { + $originalDocumentHasDoi = $publication->getData('originalDocumentHasDoi'); + if (is_null($originalDocumentHasDoi)) { $errors['originalDocumentHasDoi'] = [__('plugins.generic.scieloTranslationsFields.error.originalDocumentHasDoi.required')]; } + $doiValidator = new DoiValidator(); + $originalDocumentDoi = $publication->getData('originalDocumentDoi'); + + if ($originalDocumentHasDoi && !$doiValidator->validate($originalDocumentDoi)) { + $errors['originalDocumentDoi'] = [__('plugins.generic.scieloTranslationsFields.originalDocumentDoi.invalidDoi')]; + } + return Hook::CONTINUE; } @@ -218,7 +234,7 @@ public function addResourcesToWorkflow($hookName, $params) return Hook::CONTINUE; } - $translationDataForm = $this->getTranslationDataForm($submission, $request); + $translationDataForm = $this->getTranslationDataForm($submission, $request, 'workflow'); $components = $templateMgr->getState('components'); $components[$translationDataForm->id] = $translationDataForm->getConfig(); @@ -236,6 +252,28 @@ public function addTranslationDataTabToWorkflow($hookName, $params) return Hook::CONTINUE; } + + public function setupTranslationsFieldsHandler($hookName, $params) + { + $request = $params[0]; + $router = $request->getRouter(); + + if (!($router instanceof \PKP\core\APIRouter)) { + return; + } + + if (str_contains($request->getRequestPath(), 'api/v1/translationsFields')) { + $handler = new TranslationsFieldsHandler(); + } + + if (!isset($handler)) { + return; + } + + $router->setHandler($handler); + $handler->getApp()->run(); + exit; + } } if (!PKP_STRICT_MODE) { diff --git a/api/v1/translationsFields/TranslationsFieldsHandler.php b/api/v1/translationsFields/TranslationsFieldsHandler.php new file mode 100644 index 0000000..d46df4d --- /dev/null +++ b/api/v1/translationsFields/TranslationsFieldsHandler.php @@ -0,0 +1,102 @@ +_handlerPath = 'translationsFields'; + $roles = [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_AUTHOR]; + $this->_endpoints = [ + 'PUT' => [ + [ + 'pattern' => $this->getEndpointPattern() . '/saveTranslationFields', + 'handler' => [$this, 'saveTranslationFields'], + 'roles' => $roles + ], + ], + ]; + parent::__construct(); + } + + public function authorize($request, &$args, $roleAssignments) + { + $rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES); + + foreach ($roleAssignments as $role => $operations) { + $rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations)); + } + $this->addPolicy($rolePolicy); + + return parent::authorize($request, $args, $roleAssignments); + } + + private function getSubmission($slimRequest) + { + $queryParams = $slimRequest->getQueryParams(); + $submissionId = (int) $queryParams['submissionId']; + + return Repo::submission()->get($submissionId); + } + + private function validateOriginalDoi($originalDocumentDoi) + { + $doiValidator = new DoiValidator(); + return $doiValidator->validate($originalDocumentDoi); + } + + public function saveTranslationFields($slimRequest, $response, $args) + { + $requestParams = $slimRequest->getParsedBody(); + $placedOn = $slimRequest->getQueryParams()['placedOn']; + $submission = $this->getSubmission($slimRequest); + $publication = $submission->getCurrentPublication(); + + $originalDocumentHasDoi = $requestParams['originalDocumentHasDoi']; + $originalDocumentDoi = $requestParams['originalDocumentDoi']; + $originalDocumentCitation = null; + + if ($originalDocumentHasDoi) { + if ($this->validateOriginalDoi($originalDocumentDoi)) { + $doiClient = new DoiClient(); + $originalDocumentCitation = $doiClient->getApaCitation($originalDocumentDoi); + } elseif ($placedOn == 'workflow') { + return $response->withStatus(400)->withJson([ + 'originalDocumentDoi' => [__('plugins.generic.scieloTranslationsFields.originalDocumentDoi.invalidDoi')] + ]); + } + } + + Repo::publication()->edit($publication, [ + 'originalDocumentHasDoi' => $originalDocumentHasDoi, + 'originalDocumentDoi' => $originalDocumentDoi, + 'originalDocumentCitation' => $originalDocumentCitation + ]); + + $submission = Repo::submission()->get($submission->getId()); + $publication = $submission->getCurrentPublication(); + + $contextId = $submission->getData('contextId'); + $userGroups = Repo::userGroup()->getCollector() + ->filterByContextIds([$contextId]) + ->getMany(); + + $genreDao = DAORegistry::getDAO('GenreDAO'); + $genres = $genreDao->getByContextId($contextId)->toArray(); + + return $response->withJson( + Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication), + 200 + ); + } +} diff --git a/classes/DoiValidator.php b/classes/DoiValidator.php new file mode 100644 index 0000000..d33a03c --- /dev/null +++ b/classes/DoiValidator.php @@ -0,0 +1,11 @@ +guzzleClient = $guzzleClient; + } else { + $this->guzzleClient = Application::get()->getHttpClient(); + } + } + + public function getApaCitation(string $doi): ?string + { + $doiUrl = self::DOI_URL . $doi; + + try { + $response = $this->guzzleClient->request('GET', $doiUrl, [ + 'headers' => [ + 'Accept' => 'text/x-bibliography; style=apa' + ], + ]); + + return $response->getBody()->getContents(); + } catch (ClientException $e) { + $errorMsg = $e->getResponse()->getBody()->getContents(); + error_log("Error while getting DOI citation: $errorMsg"); + } + + return null; + } +} diff --git a/classes/components/forms/TranslationDataForm.php b/classes/components/forms/TranslationDataForm.php index 69fe04e..29ecdde 100644 --- a/classes/components/forms/TranslationDataForm.php +++ b/classes/components/forms/TranslationDataForm.php @@ -5,17 +5,19 @@ use PKP\components\forms\FormComponent; use PKP\components\forms\FieldOptions; use PKP\components\forms\FieldText; +use PKP\components\forms\FieldHTML; class TranslationDataForm extends FormComponent { public $id = 'translationData'; public $method = 'PUT'; - public function __construct($action, $submission) + public function __construct($action, $submission, $placedOn) { $publication = $submission->getCurrentPublication(); + $originalDocumentDoi = $publication->getData('originalDocumentDoi'); - $this->action = $action; + $this->action = $action . "&placedOn=$placedOn"; $this->addField(new FieldOptions('originalDocumentHasDoi', [ 'label' => __('plugins.generic.scieloTranslationsFields.originalDocumentHasDoi'), 'description' => __('plugins.generic.scieloTranslationsFields.originalDocumentHasDoi.description'), @@ -37,8 +39,22 @@ public function __construct($action, $submission) 'label' => __('plugins.generic.scieloTranslationsFields.originalDocumentDoi'), 'description' => __('plugins.generic.scieloTranslationsFields.originalDocumentDoi.description'), 'isMultilingual' => false, - 'value' => $publication->getData('originalDocumentDoi'), + 'value' => $originalDocumentDoi, 'showWhen' => ['originalDocumentHasDoi', 1] ])); + + if (!empty($originalDocumentDoi) && $placedOn == 'workflow') { + $originalDocumentCitation = $publication->getData('originalDocumentCitation'); + + if (empty($originalDocumentCitation)) { + $originalDocumentCitation = __('plugins.generic.scieloTranslationsFields.originalDocumentCitation.couldntRetrieve'); + } + + $this->addField(new FieldHTML('originalDocumentDoi', [ + 'label' => __('plugins.generic.scieloTranslationsFields.originalDocumentCitation'), + 'description' => "

{$originalDocumentCitation}

", + 'showWhen' => ['originalDocumentHasDoi', 1] + ])); + } } } diff --git a/cypress/tests/Test2_originalDoiField.cy.js b/cypress/tests/Test2_originalDoiField.cy.js index dca8936..852acd2 100644 --- a/cypress/tests/Test2_originalDoiField.cy.js +++ b/cypress/tests/Test2_originalDoiField.cy.js @@ -90,7 +90,14 @@ describe('SciELO Translations Fields - Original DOI features', function () { cy.get('input[name="originalDocumentHasDoi"][value="1"]').check(); cy.contains('label', 'DOI'); cy.contains('Please insert the DOI of the original document this submission is translating'); - cy.get('input[name="originalDocumentDoi"]').type(submissionData.originalDoi, {delay: 0}); + cy.get('input[name="originalDocumentDoi"]').type('Invalid DOI', {delay: 0}); + Cypress._.times(4, () => { + cy.contains('button', 'Continue').click(); + }); + cy.contains('The DOI entered is invalid. Please include only the identifier (e.g. "10.1234/ExampleDOI")'); + + cy.contains('.pkpSteps__step__label', 'Details').click(); + cy.get('input[name="originalDocumentDoi"]').clear().type(submissionData.originalDoi, {delay: 0}); Cypress._.times(4, () => { cy.contains('button', 'Continue').click(); }); @@ -117,5 +124,11 @@ describe('SciELO Translations Fields - Original DOI features', function () { cy.get('input[name="originalDocumentHasDoi"][value="1"]').should('be.checked'); cy.contains('DOI') cy.get('input[name="originalDocumentDoi"]').should('have.value', submissionData.originalDoi); + + cy.get('input[name="originalDocumentDoi"]').clear().type('Invalid DOI', {delay: 0}); + cy.get('#translationData').within(() => { + cy.contains('button', 'Save').click(); + }); + cy.contains('The DOI entered is invalid. Please include only the identifier (e.g. "10.1234/ExampleDOI")'); }); }); \ No newline at end of file diff --git a/cypress/tests/Test3_originalCitation.cy.js b/cypress/tests/Test3_originalCitation.cy.js new file mode 100644 index 0000000..fe2b5c3 --- /dev/null +++ b/cypress/tests/Test3_originalCitation.cy.js @@ -0,0 +1,109 @@ +import '../support/commands.js'; + +function beginSubmission(submissionData) { + cy.get('input[name="locale"][value="en"]').click(); + cy.setTinyMceContent('startSubmission-title-control', submissionData.title); + + cy.get('input[name="submissionRequirements"]').check(); + cy.get('input[name="privacyConsent"]').check(); + cy.contains('button', 'Begin Submission').click(); +} + +function detailsStep(submissionData) { + cy.setTinyMceContent('titleAbstract-abstract-control-en', submissionData.abstract); + submissionData.keywords.forEach(keyword => { + cy.get('#titleAbstract-keywords-control-en').type(keyword, {delay: 0}); + cy.wait(500); + cy.get('#titleAbstract-keywords-control-en').type('{enter}', {delay: 0}); + }); + cy.contains('button', 'Continue').click(); +} + +function contributorsStep(submissionData) { + submissionData.contributors.forEach(authorData => { + cy.contains('button', 'Add Contributor').click(); + cy.get('input[name="givenName-en"]').type(authorData.given, {delay: 0}); + cy.get('input[name="familyName-en"]').type(authorData.family, {delay: 0}); + cy.get('input[name="email"]').type(authorData.email, {delay: 0}); + cy.get('select[name="country"]').select(authorData.country); + + cy.get('.modal__panel:contains("Add Contributor")').find('button').contains('Save').click(); + cy.waitJQuery(); + }); + + cy.contains('button', 'Continue').click(); +} + +describe('SciELO Translations Fields - Original document citation features', function () { + let submissionData; + + before(function () { + submissionData = { + title: "Voodoo Child", + abstract: 'Great guitar solos', + keywords: ['guitar'], + originalDoi: '10.1590/0037-8682-0167-2020', + originalDoiCitationPart: 'COVID-19 in Brazil: advantages of a socialized unified health system and preparation to contain cases.', + contributors: [ + { + 'given': 'Jimi', + 'family': 'Hendrix', + 'email': 'jimi.hendrix@outlook.com', + 'country': 'United States' + } + ], + files: [ + { + 'file': 'dummy.pdf', + 'fileName': 'dummy.pdf', + 'mimeType': 'application/pdf', + 'genre': 'Preprint Text' + } + ] + } + }); + + it('Creates a new submission with a deposited original DOI', function() { + cy.login('ckwantes', null, 'publicknowledge'); + cy.get('div#myQueue a:contains("New Submission")').click(); + + beginSubmission(submissionData); + cy.get('input[name="originalDocumentHasDoi"][value="1"]').check(); + cy.get('input[name="originalDocumentDoi"]').type('Invalid DOI', {delay: 0}); + detailsStep(submissionData); + cy.addSubmissionGalleys(submissionData.files); + cy.contains('button', 'Continue').click(); + contributorsStep(submissionData); + cy.contains('button', 'Continue').click(); + + cy.contains('The DOI entered is invalid'); + cy.contains('h4', 'Original document citation').should('not.exist'); + + cy.contains('.pkpSteps__step__label', 'Details').click(); + cy.get('input[name="originalDocumentDoi"]').clear().type(submissionData.originalDoi, {delay: 0}); + Cypress._.times(4, () => { + cy.contains('button', 'Continue').click(); + }); + + cy.contains('The original document has a DOI'); + cy.contains(submissionData.originalDoi); + cy.contains(submissionData.originalDoiCitationPart); + + cy.contains('button', 'Submit').click(); + cy.get('.modal__panel:visible').within(() => { + cy.contains('button', 'Submit').click(); + }); + cy.waitJQuery(); + cy.contains('h1', 'Submission complete'); + }); + it('Original document citation is shown at workflow', function () { + cy.login('ckwantes', null, 'publicknowledge'); + cy.findSubmission('myQueue', submissionData.title); + + cy.get('#publication-button').click(); + cy.get('#translationData-button').click(); + + cy.contains('Original document citation'); + cy.contains(submissionData.originalDoiCitationPart); + }); +}); \ No newline at end of file diff --git a/locale/en/locale.po b/locale/en/locale.po index 3c8c24a..1d42e56 100644 --- a/locale/en/locale.po +++ b/locale/en/locale.po @@ -41,5 +41,14 @@ msgstr "DOI" msgid "plugins.generic.scieloTranslationsFields.originalDocumentDoi.description" msgstr "Please insert the DOI of the original document this submission is translating" +msgid "plugins.generic.scieloTranslationsFields.originalDocumentDoi.invalidDoi" +msgstr "The DOI entered is invalid. Please include only the identifier (e.g. \"10.1234/ExampleDOI\")" + +msgid "plugins.generic.scieloTranslationsFields.originalDocumentCitation" +msgstr "Original document citation" + +msgid "plugins.generic.scieloTranslationsFields.originalDocumentCitation.couldntRetrieve" +msgstr "It was not possible to obtain the citation of the DOI informed" + msgid "plugins.generic.scieloTranslationsFields.error.originalDocumentHasDoi.required" msgstr "You must inform if the original document has a DOI" diff --git a/locale/es/locale.po b/locale/es/locale.po index f97729d..ef1de09 100644 --- a/locale/es/locale.po +++ b/locale/es/locale.po @@ -41,5 +41,14 @@ msgstr "DOI" msgid "plugins.generic.scieloTranslationsFields.originalDocumentDoi.description" msgstr "Por favor ingresse el DOI del documento original que este envío está traduciendo" +msgid "plugins.generic.scieloTranslationsFields.originalDocumentDoi.invalidDoi" +msgstr "El DOI introducido no es válido. Por favor, introduzca sólo el identificador (ex.: \"10.1234/ExemploDOI\")" + +msgid "plugins.generic.scieloTranslationsFields.originalDocumentCitation" +msgstr "Cita del documento original" + +msgid "plugins.generic.scieloTranslationsFields.originalDocumentCitation.couldntRetrieve" +msgstr "No fue posible obtener la cita del DOI informado" + msgid "plugins.generic.scieloTranslationsFields.error.originalDocumentHasDoi.required" msgstr "Debe informar si el documento original tiene DOI" diff --git a/locale/pt_BR/locale.po b/locale/pt_BR/locale.po index 5561dcd..d6f3263 100644 --- a/locale/pt_BR/locale.po +++ b/locale/pt_BR/locale.po @@ -41,5 +41,14 @@ msgstr "DOI" msgid "plugins.generic.scieloTranslationsFields.originalDocumentDoi.description" msgstr "Por favor, insira o DOI do documento original que esta submissão está traduzindo" +msgid "plugins.generic.scieloTranslationsFields.originalDocumentDoi.invalidDoi" +msgstr "O DOI inserido é inválido. Por favor, insira apenas o identificador (ex.: \"10.1234/ExemploDOI\")" + +msgid "plugins.generic.scieloTranslationsFields.originalDocumentCitation" +msgstr "Citação do documento original" + +msgid "plugins.generic.scieloTranslationsFields.originalDocumentCitation.couldntRetrieve" +msgstr "Não foi possível obter a citação do DOI informado" + msgid "plugins.generic.scieloTranslationsFields.error.originalDocumentHasDoi.required" msgstr "Você deve informar se o documento original possui um DOI" diff --git a/templates/review/translationDataFields.tpl b/templates/review/translationDataFields.tpl index 50e0a0a..20a3109 100644 --- a/templates/review/translationDataFields.tpl +++ b/templates/review/translationDataFields.tpl @@ -39,10 +39,30 @@ {translate key="plugins.generic.scieloTranslationsFields.originalDocumentDoi"}
-