-
Notifications
You must be signed in to change notification settings - Fork 3
feat: ORCID deposition for codecheckers + CODECHECK form in reviewer tab (#16) #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,19 +9,24 @@ | |
| use APP\plugins\generic\codecheck\classes\migration\CodecheckSchemaMigration; | ||
| use APP\plugins\generic\codecheck\classes\Submission\Schema; | ||
| use APP\plugins\generic\codecheck\classes\Submission\SubmissionWizardHandler; | ||
| use APP\plugins\generic\codecheck\classes\Orcid\OrcidAuthHandler; | ||
| use APP\plugins\generic\codecheck\classes\Orcid\OrcidDepositService; | ||
| use APP\plugins\generic\codecheck\classes\Log\CodecheckLogger; | ||
| use PKP\plugins\GenericPlugin; | ||
| use PKP\plugins\Hook; | ||
| use PKP\components\forms\FieldOptions; | ||
| use APP\facades\Repo; | ||
| use APP\plugins\generic\codecheck\api\v1\CodecheckApiHandler; | ||
| use PKP\core\JSONMessage; | ||
| use APP\plugins\generic\codecheck\classes\Constants; | ||
| use APP\plugins\generic\codecheck\controllers\page\CodecheckPageHandler; | ||
|
|
||
| class CodecheckPlugin extends GenericPlugin | ||
| { | ||
| private CodecheckSchemaMigration $migration; | ||
|
|
||
| public function register($category, $path, $mainContextId = null): bool | ||
| { | ||
| error_log('[CodecheckPlugin] register() called, path=' . $path); | ||
|
|
||
| $success = parent::register($category, $path); | ||
|
|
||
| if ($success && $this->getEnabled()) { | ||
|
|
@@ -40,6 +45,8 @@ public function register($category, $path, $mainContextId = null): bool | |
| Hook::add('Submission::validate', $this->saveWizardFieldsFromRequest(...)); | ||
| // Add hook for Ajax API calls | ||
| Hook::add('Dispatcher::dispatch', [$this, 'setupAPIHandler']); | ||
| // Add hook for the custom CODECHECK Pages | ||
| Hook::add('LoadHandler', $this->setCodecheckPageHandler(...)); | ||
| // Add hook for the Template Manager | ||
| Hook::add('TemplateManager::display', $this->callbackTemplateManagerDisplay(...)); | ||
|
|
||
|
|
@@ -60,32 +67,99 @@ public function register($category, $path, $mainContextId = null): bool | |
| Hook::add('Template::SubmissionWizard::Section::Review', function($hookName, $params) use ($codecheckWizard) { | ||
| return $codecheckWizard->addToSubmissionWizardReviewTemplate($hookName, $params); | ||
| }); | ||
|
|
||
| // ORCID: automatically deposit when an article is published | ||
| Hook::add('Publication::publish', $this->onPublicationPublish(...)); | ||
| } | ||
|
|
||
| return $success; | ||
| } | ||
|
|
||
| /** | ||
| * Triggered when an editor publishes an article. | ||
| */ | ||
| public function onPublicationPublish(string $hookName, array $args): bool | ||
| { | ||
| $publication = $args[0]; | ||
| $submission = Repo::submission()->get($publication->getData('submissionId')); | ||
|
|
||
| if (!$submission) return false; | ||
| if (!$submission->getData('codecheckOptIn')) return false; | ||
|
|
||
| $context = Application::get()->getRequest()->getContext(); | ||
| if (!$this->getSetting($context->getId(), Constants::ORCID_ENABLED)) return false; | ||
|
|
||
| try { | ||
| $depositService = new OrcidDepositService($this); | ||
| $results = $depositService->depositForSubmission($submission->getId()); | ||
| foreach ($results as $result) { | ||
| if ($result['status'] === 'success') { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please make a proposal how to include user facing messaging about the results and failures here. If that means we need a database table to capture the deposition state, then that is fine.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I already store the deposition state in the The results are surfaced to editors in the CODECHECK workflow tab via the ORCID Deposition section, which shows each codechecker's current status badge (NOT AUTHORISED / PENDING / DEPOSITED / FAILED) and displays the error message inline when a deposit fails. This is loaded via the |
||
| CodecheckLogger::info('ORCID deposited for ' . $result['orcidId'] . ' put-code=' . $result['putCode']); | ||
| } else { | ||
| CodecheckLogger::error('ORCID deposit failed for ' . $result['orcidId'] . ': ' . ($result['error'] ?? 'unknown')); | ||
| } | ||
| } | ||
| } catch (\Throwable $e) { | ||
| CodecheckLogger::error('ORCID deposit exception on publish: ' . $e->getMessage()); | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Setup the CodecheckApiHandler. | ||
| * The constructor handles the request and exits — no need to set a router handler. | ||
| */ | ||
| public function setupAPIHandler(string $hookName, array $args): void | ||
| { | ||
| $request = $args[0]; | ||
| $router = $request->getRouter(); | ||
| $router = $request->getRouter(); | ||
|
|
||
| if (!($router instanceof \PKP\core\APIRouter)) { | ||
| return; | ||
| } | ||
| if (!($router instanceof \PKP\core\APIRouter)) return; | ||
|
|
||
| if (str_contains($request->getRequestPath(), 'api/v1/codecheck')) { | ||
| error_log("[CODECHECK Plugin] Instanciating the CODECHECK APIHandler"); | ||
| $apiHandler = new CodecheckApiHandler($request); | ||
| error_log("[CODECHECK Plugin] API request: " . $request->getRequestPath() . "\n"); | ||
| CodecheckLogger::debug('API request: ' . $request->getRequestPath()); | ||
| new CodecheckApiHandler($this, $request); | ||
| // Constructor handles the response and exits internally | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Declare the handler function to process the actual page PATH. | ||
| */ | ||
| public function setCodecheckPageHandler($hookName, $args) | ||
| { | ||
| $request = Application::get()->getRequest(); | ||
|
|
||
| $page = &$args[0]; | ||
| $op = &$args[1]; | ||
| $handler = &$args[3]; | ||
|
|
||
| $path = $page; | ||
| if ($op !== 'index') { | ||
| $path .= "/{$op}"; | ||
| } | ||
| if ($ops = $request->getRequestedArgs()) { | ||
| $path .= '/' . implode('/', $ops); | ||
| } | ||
|
|
||
| if (!isset($apiHandler)) { | ||
| return; | ||
| // ORCID OAuth routes | ||
| if ($page === 'codecheck' && $op === 'orcid') { | ||
| $subOp = $request->getRequestedArgs()[0] ?? ''; | ||
| if (in_array($subOp, ['startAuth', 'callback'], true)) { | ||
| $handler = new OrcidAuthHandler($this); | ||
| $args[1] = $subOp; | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| $router->setHandler($apiHandler); | ||
| exit; | ||
| if ($page = 'codecheck' && $op == 'info') { | ||
| $page = 'pages'; | ||
| $op = 'view'; | ||
| $handler = new CodecheckPageHandler($this); | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private function addAssets(): void | ||
|
|
@@ -121,12 +195,36 @@ public function callbackTemplateManagerDisplay($hookName, $args): bool | |
| { | ||
| $templateMgr = $args[0]; | ||
| $request = Application::get()->getRequest(); | ||
|
|
||
| if ($request->getRequestedOp() == 'workflow') { | ||
|
|
||
| // ---------------------------------------------------------------- | ||
| // Editor workflow page | ||
|
nuest marked this conversation as resolved.
|
||
| // ---------------------------------------------------------------- | ||
| if ($request->getRequestedOp() == 'editorial' && $request->getRequestedPage() == 'dashboard') { | ||
| $context = $request->getContext(); | ||
| $contextId = $context->getId(); | ||
|
|
||
| $orcidAuthUrl = $request->getBaseUrl() . '/index.php/' . $context->getPath() . '/codecheck/orcid/startAuth'; | ||
|
|
||
| $orcidConfig = json_encode([ | ||
| 'enabled' => (bool) $this->getSetting($contextId, Constants::ORCID_ENABLED), | ||
| 'authUrl' => $orcidAuthUrl, | ||
| 'apiType' => $this->getSetting($contextId, Constants::ORCID_API_TYPE) | ||
| ?? Constants::ORCID_API_TYPE_SANDBOX, | ||
| 'apiBaseUrl' => $request->getBaseUrl() . '/index.php/' . $context->getPath(), | ||
| ]); | ||
|
|
||
| $templateMgr->addJavaScript( | ||
| 'codecheck-orcid-config', | ||
| 'window.codecheckOrcidConfig = ' . $orcidConfig . ';', | ||
| [ | ||
| 'inline' => true, | ||
| 'contexts' => ['backend'], | ||
| 'priority' => TemplateManager::STYLE_SEQUENCE_LAST | ||
| ] | ||
| ); | ||
|
|
||
| $submission = $request->getRouter()->getHandler()->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION); | ||
|
|
||
| if ($submission) { | ||
| $publication = $submission->getCurrentPublication(); | ||
| $templateMgr->setState([ | ||
| 'codecheckSubmission' => [ | ||
| 'id' => $submission->getId(), | ||
|
|
@@ -136,14 +234,59 @@ public function callbackTemplateManagerDisplay($hookName, $args): bool | |
| 'dataRepository' => $submission->getData('dataRepository'), | ||
| 'manifestFiles' => $submission->getData('manifestFiles'), | ||
| 'dataAvailabilityStatement' => $submission->getData('dataAvailabilityStatement'), | ||
| ] | ||
| ], | ||
| ]); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| // ---------------------------------------------------------------- | ||
| // Reviewer page — inject submission data + ORCID config for Vue | ||
| // ---------------------------------------------------------------- | ||
| if ($request->getRequestedPage() == 'reviewer' && $request->getRequestedOp() == 'submission') { | ||
| $requestArgs = $request->getRequestedArgs(); | ||
| $submissionId = (int) ($requestArgs[0] ?? 0); | ||
|
|
||
| if ($submissionId) { | ||
| $context = $request->getContext(); | ||
| $contextId = $context->getId(); | ||
| $submission = Repo::submission()->get($submissionId); | ||
|
|
||
| if ($submission && $submission->getData('codecheckOptIn')) { | ||
| $orcidAuthUrl = $request->getBaseUrl() . '/index.php/' . $context->getPath() . '/codecheck/orcid/startAuth'; | ||
|
|
||
| $reviewerData = json_encode([ | ||
| 'submissionId' => $submission->getId(), | ||
| 'codecheckOptIn' => true, | ||
| 'orcid' => [ | ||
| 'enabled' => (bool) $this->getSetting($contextId, Constants::ORCID_ENABLED), | ||
| 'authUrl' => $orcidAuthUrl, | ||
| 'apiType' => $this->getSetting($contextId, Constants::ORCID_API_TYPE) ?? Constants::ORCID_API_TYPE_SANDBOX, | ||
| 'apiBaseUrl' => $request->getBaseUrl() . '/index.php/' . $context->getPath(), | ||
| ], | ||
| ]); | ||
|
|
||
| $templateMgr->addJavaScript( | ||
| 'codecheck-reviewer-data', | ||
| 'window.codecheckReviewerData = ' . $reviewerData . ';', | ||
| [ | ||
| 'inline' => true, | ||
| 'contexts' => ['backend'], | ||
| 'priority' => TemplateManager::STYLE_SEQUENCE_LAST, | ||
| ] | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| public function getUrlPageRoute(string $page): string | ||
| { | ||
| $request = Application::get()->getRequest(); | ||
| return $request->getDispatcher()->url($request, ROUTE_PAGE, null, $page); | ||
| } | ||
|
|
||
| public function addOptInToSchema(string $hookName, array $args): bool | ||
| { | ||
| $schema = $args[0]; | ||
|
|
@@ -166,18 +309,27 @@ public function addOptInToSchema(string $hookName, array $args): bool | |
| public function addOptInCheckbox(string $hookName, \PKP\components\forms\FormComponent $form): bool | ||
| { | ||
| if ($form->id === 'submitStart' || $form->id === 'submissionStart' || str_contains($form->id, 'start')) { | ||
| $request = Application::get()->getRequest(); | ||
| $context = $request->getContext(); | ||
| $codecheckMode = $this->getSetting($context->getId(), Constants::CODECHECK_MODE); | ||
| $checkboxValue = false; | ||
|
|
||
| if ($codecheckMode == 'opt-out') { | ||
| $checkboxValue = true; | ||
| } | ||
|
|
||
| $form->addField(new FieldOptions('codecheckOptIn', [ | ||
| 'label' => __('plugins.generic.codecheck.displayName'), | ||
| 'type' => 'checkbox', | ||
| 'options' => [ | ||
| [ | ||
| 'value' => 1, | ||
| 'label' => __('plugins.generic.codecheck.optIn.description', [ | ||
| 'codecheckLink' => '<a href="https://codecheck.org.uk/" target="_blank">CODECHECK</a>' | ||
| 'codecheckLink' => "<a href='{$this->getUrlPageRoute("codecheck")}/info' target='_blank'>CODECHECK</a>" | ||
| ]) | ||
| ] | ||
| ], | ||
| 'value' => false, | ||
| 'value' => $checkboxValue, | ||
| 'groupId' => 'default' | ||
| ])); | ||
|
|
||
|
|
@@ -202,10 +354,7 @@ public function saveOptIn(string $hookName, array $params): bool | |
| public function saveWizardFieldsFromRequest(string $hookName, array $params): bool | ||
| { | ||
| $submission = $params[1]; | ||
|
|
||
| if (!$submission) { | ||
| return false; | ||
| } | ||
| if (!$submission) return false; | ||
|
|
||
| $request = Application::get()->getRequest(); | ||
|
|
||
|
|
@@ -248,13 +397,6 @@ public function getActions($request, $actionArgs): array | |
| return $actions->execute($request, $actionArgs, parent::getActions($request, $actionArgs)); | ||
| } | ||
|
|
||
| /** | ||
| * Load a form when the `settings` button is clicked and | ||
| * save the form when the user saves it. | ||
| * | ||
| * @param array $args | ||
| * @param Request $request | ||
| */ | ||
| public function manage($args, $request): JSONMessage | ||
| { | ||
| $manage = new Manage($this); | ||
|
|
@@ -266,12 +408,19 @@ public function setEnabled($enabled, $contextId = null) | |
| $result = parent::setEnabled($enabled, $contextId); | ||
|
|
||
| if ($enabled) { | ||
| $migration = new CodecheckSchemaMigration(); | ||
| $migration->up(); | ||
| $this->migration = new CodecheckSchemaMigration(); | ||
| $this->migration->up(); | ||
| } | ||
|
|
||
| return $result; | ||
| } | ||
|
|
||
| public function resetSchema(): void | ||
| { | ||
| $this->migration = new CodecheckSchemaMigration(); | ||
| $this->migration->down(); | ||
| $this->migration->up(); | ||
| } | ||
| } | ||
|
|
||
| if (!PKP_STRICT_MODE) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This ssems to require that the ORCID credentials must be provided by codecheckers before the publication.
_Is it also possible that a CODECHECK goes to the OJS backend after the article is published and then triggers the deposition?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, absolutely. The automatic deposition on publication is just a convenience, if the codechecker has not authorised before publication, or if the deposit fails, editors can still trigger it manually at any time using the "Deposit to ORCID" button in the CODECHECK workflow tab. The button remains available after publication so codecheckers can authorise and deposit retroactively.