diff --git a/CodecheckPlugin.php b/CodecheckPlugin.php index be26c7d4..2cf15265 100644 --- a/CodecheckPlugin.php +++ b/CodecheckPlugin.php @@ -21,6 +21,7 @@ use APP\plugins\generic\codecheck\controllers\page\CodecheckPageHandler; use APP\plugins\generic\codecheck\classes\CodecheckRoles\CodecheckRoleArray; use APP\plugins\generic\codecheck\classes\CodecheckRoles\CodecheckRoleManager; +use APP\core\Request; class CodecheckPlugin extends GenericPlugin { @@ -249,7 +250,7 @@ public function addOptInCheckbox(string $hookName, \PKP\components\forms\FormCom $request = Application::get()->getRequest(); $context = $request->getContext(); $codecheckMode = $this->getSetting($context->getId(), Constants::CODECHECK_MODE); - error_log("[CODECHECK Settings] Mode: " . $codecheckMode); + CodecheckLogger::debug("Settings Mode: " . $codecheckMode); $checkboxValue = false; $checkboxDisabled = false; $codecheckDescription = __('plugins.generic.codecheck.optIn.description', [ @@ -380,11 +381,13 @@ public function manage($args, $request): JSONMessage public function setEnabled($enabled, $contextId = null) { + CodecheckLogger::debug("Plugin Enabled!"); $result = parent::setEnabled($enabled, $contextId); if ($enabled) { - $this->migration = new CodecheckSchemaMigration(); - $this->migration->up(); + $migration = new CodecheckSchemaMigration(); + $migration->up(); + $migration->issueLabelsUp(); } return $result; @@ -395,6 +398,8 @@ public function resetSchema(): void $this->migration = new CodecheckSchemaMigration(); $this->migration->down(); $this->migration->up(); + $this->migration->issueLabelsDown(); + $this->migration->issueLabelsUp(); } } diff --git a/api/v1/CodecheckApiHandler.php b/api/v1/CodecheckApiHandler.php index 447cd257..c97218f3 100644 --- a/api/v1/CodecheckApiHandler.php +++ b/api/v1/CodecheckApiHandler.php @@ -7,12 +7,10 @@ use APP\plugins\generic\codecheck\classes\Exceptions\ApiCreateException; use APP\plugins\generic\codecheck\classes\Exceptions\ApiFetchException; use APP\plugins\generic\codecheck\classes\Exceptions\NoMatchingIssuesFoundException; -use APP\plugins\generic\codecheck\classes\CodecheckRegister\CodecheckVenueTypes; -use APP\plugins\generic\codecheck\classes\CodecheckRegister\CodecheckVenueNames; use APP\plugins\generic\codecheck\classes\CodecheckRegister\CodecheckGithubRegisterApiClient; use APP\plugins\generic\codecheck\classes\CodecheckRegister\CertificateIdentifierList; use APP\plugins\generic\codecheck\classes\CodecheckRegister\CertificateIdentifier; -use APP\plugins\generic\codecheck\classes\CodecheckRegister\CodecheckVenue; +use APP\plugins\generic\codecheck\classes\CodecheckRegister\CodecheckGithubRegisterIssue; use APP\plugins\generic\codecheck\classes\Workflow\CodecheckMetadataHandler; use APP\plugins\generic\codecheck\classes\Workflow\CodecheckYamlValidator; use APP\plugins\generic\codecheck\classes\Log\CodecheckLogger; @@ -23,6 +21,11 @@ use \Github\Client; use APP\plugins\generic\codecheck\classes\CodecheckRoles\CodecheckRoleManager; use APP\plugins\generic\codecheck\classes\Exceptions\RoleExceptions\RoleNotFoundException; +use APP\plugins\generic\codecheck\classes\Exceptions\CurlExceptions\CurlInitException; +use APP\plugins\generic\codecheck\classes\Exceptions\CurlExceptions\CurlReadException; +use APP\plugins\generic\codecheck\classes\CodecheckRegister\CodecheckIssueLabels; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; class CodecheckApiHandler { @@ -57,9 +60,9 @@ public function __construct(CodecheckPlugin $plugin, Request $request, Codecheck $this->endpoints = [ 'GET' => [ [ - 'route' => 'venue', - 'handler' => [$this, 'getVenueData'], - 'roles' => $roles->readMetadata(), + 'route' => 'labels', + 'handler' => [$this, 'getCodecheckIssueLabels'], + 'roles' => $roles->editMetadata(), ], [ 'route' => 'metadata', @@ -83,6 +86,11 @@ public function __construct(CodecheckPlugin $plugin, Request $request, Codecheck 'handler' => [$this, 'reserveIdentifier'], 'roles' => $roles->editMetadata(), ], + [ + 'route' => 'issue', + 'handler' => [$this, 'updateGithubIssue'], + 'roles' => $roles->editMetadata(), + ], [ 'route' => 'metadata', 'handler' => [$this, 'saveMetadata'], @@ -122,7 +130,7 @@ private function getEndpoint(): ApiEndpoint // get the request Method like POST or GET $requestMethod = $this->request->getRequestMethod(); - error_log("Method: " . $requestMethod); + CodecheckLogger::debug("API Method: " . $requestMethod); return new ApiEndpoint($this->endpoints, $this->route, $requestMethod); } @@ -202,45 +210,91 @@ private function serveRequest(): void } /** - * Gets Venue Types and Venue Names + * Gets the Issue Labels of the CODECHECK API * * @return void */ - private function getVenueData(): void - { - try { - $codecheckVenueTypes = new CodecheckVenueTypes(); - } catch (\Throwable $e) { - JsonResponse::staticResponse([ - 'success' => false, - 'error' => "Error while fetching the Venue Types: " . $e->getMessage(), - ], 400); - return; + private function getCodecheckIssueLabels(): void + { + $dbLabelsOutdated = false; + + $issueLabelsLastUpdated = strtotime($this->getIssueLabelsLastUpdated()); + $now = strtotime(date('Y-m-d H:i:s')); + $timeDifferenceInHours = round(($now - $issueLabelsLastUpdated) / 3600); + + if($timeDifferenceInHours > 6) { + $dbLabelsOutdated = true; } - try { - $codecheckVenueNames = new CodecheckVenueNames(); - } catch (\Throwable $e) { - JsonResponse::staticResponse([ - 'success' => false, - 'error' => "Error while fetching the Venue Names: " . $e->getMessage(), - ], 400); - return; + $codecheckIssueLabels = CodecheckIssueLabels::fromDB(); + + if($dbLabelsOutdated) { + try { + $codecheckIssueLabels = CodecheckIssueLabels::fromApi("https://codecheck.org.uk/register/venues/index.json"); + } catch (\Throwable $e) { + JsonResponse::staticResponse([ + 'success' => false, + 'error' => $e->getMessage(), + ], 400); + return; + } } - // get the github custom labels specified in the plugin settings form + // add the github custom labels specified in the plugin settings form to the Label Array returned back to the user $context = $this->request->getContext(); $githubCustomLabels = $this->plugin->getSetting($context->getId(), Constants::CODECHECK_GITHUB_CUSTOM_LABELS); + $codecheckIssueLabels->addLabelArray($githubCustomLabels); - // Serve the getVenueData API route + // Serve the getCodecheckIssueLabels API route JsonResponse::staticResponse([ 'success' => true, - 'venueTypes' => $codecheckVenueTypes->get()->toArray(), - 'venueNames' => $codecheckVenueNames->get()->toArray(), - 'customLabels' => $githubCustomLabels, + 'labels' => $codecheckIssueLabels->get()->toArray(), ], 200); } + private function getAuthorStringBasedOnAuthorAnonymity(): string|null + { + $postParams = json_decode(file_get_contents('php://input'), true); + $submissionData = $postParams["submission"]; + $authorString = $submissionData["authorString"]; + + $context = $this->request->getContext(); + $isAuthorStringEnabled = $this->plugin->getSetting($context->getId(), Constants::CODECHECK_AUTHOR_ANONYMITY); + + // if Authors should be Anonymous/ if no Author string was given, set it to null + if(!$isAuthorStringEnabled || !is_string($authorString)) { + $authorString = null; + } + + return $authorString; + } + + /** + * This function gets when the Codecheck Issue Labels where last updated + * + * @return string The Date when the issues where last updated + */ + private function getIssueLabelsLastUpdated(): string + { + if (!Schema::hasTable('codecheck_issue_labels')) { + // TODO: implement what happens when the table doesn't exist + CodecheckLogger::error("CODECHECK API: The Issue Label table doesn't exist"); + } + + $labelsLastUpdated = DB::table('codecheck_issue_labels') + ->select(['labels_last_updated']) + ->first(); + + CodecheckLogger::debug("Labels: " . print_r(DB::table('codecheck_issue_labels')->select(['*'])->get()->toArray(), true)); + + // If Labels weren't updated yet, set last updated to earliest date possible, so they will definitely get updated + $labelsLastUpdated = $labelsLastUpdated->labels_last_updated ?? date('Y-m-d H:i:s', 0); + + CodecheckLogger::debug("CODECHECK API: Codecheck Issues Last Updated: " . json_encode($labelsLastUpdated)); + + return $labelsLastUpdated; + } + /** * This reserves a new Identifier * @@ -249,98 +303,307 @@ private function getVenueData(): void public function reserveIdentifier(): void { $postParams = json_decode(file_get_contents('php://input'), true); - $venueType = $postParams["venueType"]; - $venueName = $postParams["venueName"]; - $customLabels = $postParams["customLabels"]; - $authorString = $postParams["authorString"]; + $reserveIdentifierMode = $postParams['reserveIdentifierMode']; + + if(!is_string($reserveIdentifierMode)) { + JsonResponse::staticResponse([ + 'success' => false, + 'error' => "No Reserve Identifier Mode was specified.", + ], 400); + return; + } + + $issueLabelArray = $postParams["labels"]; + $submissionData = $postParams["submission"]; + $articleTitle = $submissionData["title"]; + $repositories = $postParams["repositories"]; + $codecheckers = $postParams["codecheckers"]; - // get the github Register Repository specified in the plugin settings form $context = $this->request->getContext(); $githubPersonalAccessToken = $this->plugin->getSetting($context->getId(), Constants::CODECHECK_GITHUB_PERSONAL_ACCESS_TOKEN); $githubRegisterOrganization = $this->plugin->getSetting($context->getId(), Constants::CODECHECK_GITHUB_REGISTER_ORGANIZATION); $githubRegisterRepository = $this->plugin->getSetting($context->getId(), Constants::CODECHECK_GITHUB_REGISTER_REPOSITORY); - $isAuthorStringEnabled = $this->plugin->getSetting($context->getId(), Constants::CODECHECK_AUTHOR_ANONYMITY); - error_log("[Codecheck Api Handler] GitHub Register Repository specified in the Settings form: " . $githubRegisterRepository); + $authorString = $this->getAuthorStringBasedOnAuthorAnonymity(); - // if Authors should be Anonymous/ if no Author string was given, set it to null - if(!$isAuthorStringEnabled || !is_string($authorString)) { - $authorString = null; - } + // CODECHECK GitHub Issue Register API parser + $codecheckGithubRegisterApiClient = new CodecheckGithubRegisterApiClient( + $githubPersonalAccessToken, + $githubRegisterOrganization, + $githubRegisterRepository, // Name of the GitHub Repository for the Register + $this->codecheckMetadataHandler->getSubmissionId(), // Submission ID + $context, // The Journal Object of the Submission + ); - // check if they are of type string (If not return success false over the API) - if(is_string($venueType) && is_string($venueName) && is_array($customLabels)) { - // CODECHECK GitHub Issue Register API parser - $codecheckGithubRegisterApiClient = new CodecheckGithubRegisterApiClient( - $githubPersonalAccessToken, // The GitHub PAT (classic) needed to access the Register Repository - $githubRegisterOrganization, // The organization owning the GitHub Register Repository - $githubRegisterRepository, // Name of the GitHub Repository for the Register - $this->codecheckMetadataHandler->getSubmissionId(), // Submission ID - $context, // The Journal Object of the Submission + // CODECHECK Register with list of all identifiers in range + try { + if($reserveIdentifierMode == 'linkExistingIdentifier') { + $identifierStr = $postParams["identifier"]; + $certificateIdentifierList = CertificateIdentifierList::fromApiWithIdentifier( + $codecheckGithubRegisterApiClient, + CertificateIdentifier::fromStr($identifierStr) + ); + } + $certificateIdentifierList = CertificateIdentifierList::fromApi( + $codecheckGithubRegisterApiClient, + true ); + } catch (ApiFetchException $ae) { + JsonResponse::staticResponse([ + 'success' => false, + 'error' => $ae->getMessage(), + ], 400); + return; + } catch (NoMatchingIssuesFoundException $me) { + JsonResponse::staticResponse([ + 'success' => false, + 'error' => $me->getMessage(), + ], 400); + return; + } - CodecheckLogger::debug(print_r($this->request->getContext(), true)); - - // CODECHECK Register with list of all identifiers in range - try { - $certificateIdentifierList = CertificateIdentifierList::fromApi($codecheckGithubRegisterApiClient); - } catch (ApiFetchException $ae) { - JsonResponse::staticResponse([ - 'success' => false, - 'error' => $ae->getMessage(), - ], 400); - return; - } catch (NoMatchingIssuesFoundException $me) { - JsonResponse::staticResponse([ - 'success' => false, - 'error' => $me->getMessage(), - ], 400); - return; - } + if($reserveIdentifierMode == 'linkExistingIdentifier') { + $identifierStr = $postParams["identifier"]; + $this->linkExistingIdentifier($identifierStr, $certificateIdentifierList); + return; + } - // print Certificate Identifier list + // check if they are of type string (If not return success false over the API) + if(is_array($issueLabelArray) && is_array($submissionData) && is_string($authorString) && is_string($articleTitle) && is_array($repositories) && is_array($codecheckers)) { + // sort Certificate Identifier list descending $certificateIdentifierList->sortDesc(); // create the new unique Identifier - $new_identifier = CertificateIdentifier::newUniqueIdentifier($certificateIdentifierList); - - // create the CODECHECK Venue with the selected type and name - $codecheckVenue = new CodecheckVenue($venueType, $venueName); - - // Add the new issue to the CODECHECK GtiHub Register - try { - $issueGithubUrl = $codecheckGithubRegisterApiClient->addIssue( - $new_identifier, - $codecheckVenue->getVenueType(), - $codecheckVenue->getVenueName(), - $customLabels, - $authorString, - ); - } catch (ApiCreateException $e) { - // return an error result - JsonResponse::staticResponse([ - 'success' => false, - 'error' => $e->getMessage(), - ], 400); - return; + $newIdentifier = CertificateIdentifier::newUniqueIdentifier($certificateIdentifierList); + + // create the CODECHECK Issue Labels with the selected issue labels + $codecheckIssueLabels = new CodecheckIssueLabels($issueLabelArray); + + switch ($reserveIdentifierMode) { + case 'api': + $issue = $this->reserveIdentifierWithApi( + $codecheckGithubRegisterApiClient, + $newIdentifier, + $codecheckIssueLabels, + $articleTitle, + $authorString, + $codecheckers, + $repositories + ); + $issueGithubUrl = $issue['html_url']; + $issueNumber = $issue['number']; + break; + + case 'newIssueUrl': + $issueGithubUrl = $this->reserveIdentifierWithNewIssueUrl( + $githubRegisterOrganization, + $githubRegisterRepository, + $newIdentifier, + $codecheckIssueLabels, + $articleTitle, + $authorString, + $codecheckers, + $repositories + ); + break; + + default: + JsonResponse::staticResponse([ + 'success' => false, + 'error' => "An unexpected mode for the reservation of the Certificate Identifier was given: " . $reserveIdentifierMode, + ], 400); + break; } + // check if an error happened and return if that is the case + if($issueGithubUrl == null) { return; } + // return a success result JsonResponse::staticResponse([ 'success' => true, - 'identifier' => $new_identifier->toStr(), + 'identifier' => $newIdentifier->toStr(), 'issueUrl' => $issueGithubUrl, + 'issueNumber' => $issueNumber ?? null, ], 200); return; } else { JsonResponse::staticResponse([ 'success' => false, - 'error' => "The CODECHECK Venue Type and/ or Venue Names aren't of Type string as expected.", + 'error' => "Some Parameters sent with POST to the API aren't of the expected datatype.", ], 400); return; } } + public function updateGithubIssue(): void + { + $postParams = json_decode(file_get_contents('php://input'), true); + $issue = $postParams['issue']; + if(!is_array($issue) || !is_int($issue['number']) || !is_string($issue['url'])) { + # TODO: JSON Error Response + return; + } + + $issueLabelArray = $postParams["issue"]["labelsSelected"]; + $submissionData = $postParams["submission"]; + $articleTitle = $submissionData["title"]; + $identifierStr = $postParams["identifier"]; + $repositories = $postParams["repositories"]; + $codecheckers = $postParams["codecheckers"]; + + $context = $this->request->getContext(); + $githubPersonalAccessToken = $this->plugin->getSetting($context->getId(), Constants::CODECHECK_GITHUB_PERSONAL_ACCESS_TOKEN); + $githubRegisterOrganization = $this->plugin->getSetting($context->getId(), Constants::CODECHECK_GITHUB_REGISTER_ORGANIZATION); + $githubRegisterRepository = $this->plugin->getSetting($context->getId(), Constants::CODECHECK_GITHUB_REGISTER_REPOSITORY); + + $authorString = $this->getAuthorStringBasedOnAuthorAnonymity(); + + // CODECHECK GitHub Issue Register API parser + $codecheckGithubRegisterApiClient = new CodecheckGithubRegisterApiClient( + $githubPersonalAccessToken, + $githubRegisterOrganization, + $githubRegisterRepository, // Name of the GitHub Repository for the Register + $this->codecheckMetadataHandler->getSubmissionId(), // Submission ID + $context, // The Journal Object of the Submission + ); + + if(is_string($identifierStr) && is_array($issueLabelArray) && is_array($submissionData) && is_string($authorString) && is_string($articleTitle) && is_array($repositories) && is_array($codecheckers)) { + $identifier = CertificateIdentifier::fromStr($identifierStr); + $codecheckIssueLabels = new CodecheckIssueLabels($issueLabelArray); + $updatedIssue = $codecheckGithubRegisterApiClient->updateIssue( + $issue['number'], + $identifier, + $codecheckIssueLabels, + $articleTitle, + $authorString, + $codecheckers, + $repositories + ); + + # TODO: Check if the update function worked and return JSON Error if not + + // return a success result + JsonResponse::staticResponse([ + 'success' => true, + 'identifier' => $identifier->toStr(), + 'issueUrl' => $updatedIssue['html_url'], + 'issueNumber' => $updatedIssue['number'] ?? null, + ], 200); + return; + } else { + JsonResponse::staticResponse([ + 'success' => false, + 'error' => "Some Parameters sent with POST to the API aren't of the expected datatype.", + ], 400); + return; + } + } + + /** + * This reserves a new Identifier with the GitHub API + * + * @return ?array + */ + private function reserveIdentifierWithApi( + CodecheckGithubRegisterApiClient $codecheckGithubRegisterApiClient, + CertificateIdentifier $identifier, + CodecheckIssueLabels $issueLabels, + string $articleTitle, + string $authorString, + array $codecheckers, + array $repositories + ): ?array + { + // Add the new issue to the CODECHECK GtiHub Register + try { + $issue = $codecheckGithubRegisterApiClient->addIssue( + $identifier, + $issueLabels, + $articleTitle, + $authorString, + $codecheckers, + $repositories + ); + } catch (ApiCreateException $e) { + // return an error result + JsonResponse::staticResponse([ + 'success' => false, + 'error' => $e->getMessage(), + ], 400); + return null; + } + + return $issue; + } + + /** + * This reserves a new Identifier with the GitHub New Issue Url + * + * @return string + */ + private function reserveIdentifierWithNewIssueUrl( + string $githubRegisterOrganization, + string $githubRegisterRepository, + CertificateIdentifier $identifier, + CodecheckIssueLabels $issueLabels, + string $articleTitle, + string $authorString, + array $codecheckers, + array $repositories + ): string + { + $journalName = $this->request->getContext()?->getLocalizedName() ?? 'Unknwon Journal'; + + $codecheckIssue = new CodecheckGithubRegisterIssue( + $githubRegisterOrganization, + $githubRegisterRepository, + $identifier, + $issueLabels, + $articleTitle, + $journalName, + $authorString, + $this->codecheckMetadataHandler->getSubmissionId(), + $codecheckers, + $repositories + ); + + return $codecheckIssue->getNewIssueUrl(); + } + + private function linkExistingIdentifier( + string $identifierStr, + CertificateIdentifierList $certificateIdentifierList + ) { + $title = "a | " . $identifierStr; + $rawIdentifier = CertificateIdentifierList::getRawIdentifier($title); + if($rawIdentifier == null) { + JsonResponse::staticResponse([ + 'success' => false, + 'identifier' => $identifierStr, + 'error' => "The identifier: " . $identifierStr . " isn't matching the required format (YYYY-NNN or YYYY-NNN/YYYY-NNN).", + ], 400); + return; + } + $identifier = CertificateIdentifier::fromStr($rawIdentifier); + $issue = $certificateIdentifierList->getIssueInformationByIdentifier($identifier); + if(is_string($issue['issueUrl']) && is_int($issue['issueNumber'])) { + JsonResponse::staticResponse([ + 'success' => true, + 'identifier' => $identifier->toStr(), + 'issueUrl' => $issue['issueUrl'], + 'issueNumber' => $issue['issueNumber'], + ], 200); + return; + } + + JsonResponse::staticResponse([ + 'success' => false, + 'identifier' => $identifierStr, + 'error' => "The certificate with the Identifier: ". $identifierStr . " doesn't exist in the GitHub Register.", + ], 404); + return; + } + /** * This function loads the Codecheck Metadata from an existing `codecheck.yml` in an existing Code Repository * diff --git a/classes/CodecheckRegister/CertificateIdentifierList.php b/classes/CodecheckRegister/CertificateIdentifierList.php index 5db19e65..cc1c25ad 100644 --- a/classes/CodecheckRegister/CertificateIdentifierList.php +++ b/classes/CodecheckRegister/CertificateIdentifierList.php @@ -30,13 +30,18 @@ function __construct() * @return CertificateIdentifierList Returns a new List containing all fetched Certificate Identifiers from GitHub */ static function fromApi( - CodecheckGithubRegisterApiClient $codecheckGithubRegisterApiClient + CodecheckGithubRegisterApiClient $codecheckGithubRegisterApiClient, + ?bool $onlyNewestIdentifiers ): CertificateIdentifierList { $newCertificateIdentifierList = new CertificateIdentifierList(); // fetch API try { - $codecheckGithubRegisterApiClient->fetchIssues(); + if($onlyNewestIdentifiers == true) { + $codecheckGithubRegisterApiClient->fetchNewestIssues(); + } else { + $codecheckGithubRegisterApiClient->fetchAllIssues(); + } } catch (ApiFetchException $ae) { CodecheckLogger::error($ae->getMessage()); throw $ae; @@ -45,7 +50,46 @@ static function fromApi( throw $me; } - foreach ($codecheckGithubRegisterApiClient->getIssues() as $issue) { + return CertificateIdentifierList::createNewCertificateIdentifierList( + $codecheckGithubRegisterApiClient->getIssues(), + $newCertificateIdentifierList + ); + } + + /** + * Factory Method to create a new CertificateIdentifierList from a GitHub API fetch + * + * @param CodecheckGithubRegisterApiClient $codecheckGithubRegisterApiClient The APIParser for the GitHub Issues + * @return CertificateIdentifierList Returns a new List containing all fetched Certificate Identifiers from GitHub + */ + static function fromApiWithIdentifier( + CodecheckGithubRegisterApiClient $codecheckGithubRegisterApiClient, + CertificateIdentifier $certificateIdentifier + ): CertificateIdentifierList { + $newCertificateIdentifierList = new CertificateIdentifierList(); + + // fetch API + try { + $codecheckGithubRegisterApiClient->fetchIssueByIdentifier($certificateIdentifier); + } catch (ApiFetchException $ae) { + CodecheckLogger::error($ae->getMessage()); + throw $ae; + } catch (NoMatchingIssuesFoundException $me) { + CodecheckLogger::error($me->getMessage()); + throw $me; + } + + return CertificateIdentifierList::createNewCertificateIdentifierList( + $codecheckGithubRegisterApiClient->getIssues(), + $newCertificateIdentifierList + ); + } + + private static function createNewCertificateIdentifierList( + array $issues, + CertificateIdentifierList $newCertificateIdentifierList + ): CertificateIdentifierList { + foreach ($issues as $issue) { // raw identifier (can still have ranges of identifiers); $rawIdentifier = CertificateIdentifierList::getRawIdentifier($issue['title']); @@ -56,7 +100,7 @@ static function fromApi( } // append to all identifiers in new Register - $newCertificateIdentifierList->appendToCertificateIdList($rawIdentifier); + $newCertificateIdentifierList->appendToCertificateIdList($rawIdentifier, $issue); } // return the new Register @@ -98,10 +142,11 @@ public static function getRawIdentifier(string $title): ?string /** * Appends a raw Identifier to the list of Certificate Identifiers * - * @param string $rawidentifier The raw Identifier to be appended + * @param string $rawIdentifier The raw Identifier to be appended + * @param array $issue The GitHub Issue information of the raw Identifier to be appended * @return void */ - public function appendToCertificateIdList(string $rawIdentifier): void + public function appendToCertificateIdList(string $rawIdentifier, array $issue): void { // list of certificate identifiers in range $idRange = []; @@ -111,25 +156,33 @@ public function appendToCertificateIdList(string $rawIdentifier): void // split into "fromIdStr" and "toIdStr" list($fromIdStr, $toIdStr) = explode('/', $rawIdentifier); - $from_identifier = CertificateIdentifier::fromStr($fromIdStr); - $to_identifier = CertificateIdentifier::fromStr($toIdStr); + $fromIdentifier = CertificateIdentifier::fromStr($fromIdStr); + $toIdentifier = CertificateIdentifier::fromStr($toIdStr); // append to $idRange list - for ($id_count = $from_identifier->getNumber(); $id_count <= $to_identifier->getNumber(); $id_count++) { - $new_identifier = new CertificateIdentifier($from_identifier->getYear(), $id_count); + for ($id_count = $fromIdentifier->getNumber(); $id_count <= $toIdentifier->getNumber(); $id_count++) { + $newIdentifier = new CertificateIdentifier($fromIdentifier->getYear(), $id_count); // append new identifier - $idRange[] = $new_identifier; + $idRange[] = [ + 'identifier' => $newIdentifier, + 'issueUrl' => $issue['html_url'], + 'issueNumber' => $issue['number'] + ]; } } // if it isn't a list then just append on identifier else { - $new_identifier = CertificateIdentifier::fromStr($rawIdentifier); - $idRange[] = $new_identifier; + $newIdentifier = CertificateIdentifier::fromStr($rawIdentifier); + $idRange[] = [ + 'identifier' => $newIdentifier, + 'issueUrl' => $issue['html_url'], + 'issueNumber' => $issue['number'] + ]; } // append to all certificate identifiers foreach ($idRange as $identifier) { - if (!$this->uniqueArray->contains($identifier)) { + if (!$this->uniqueArray->containsIdentifier($identifier)) { $this->uniqueArray->add($identifier); } } @@ -142,11 +195,11 @@ public function sortAsc(): void { $this->uniqueArray->sort(function($a, $b) { // First, compare year - if ($a->getYear() !== $b->getYear()) { - return $a->getYear() <=> $b->getYear(); + if ($a['identifier']->getYear() !== $b['identifier']->getYear()) { + return $a['identifier']->getYear() <=> $b['identifier']->getYear(); } // If years are equal, compare ID - return $a->getNumber() <=> $b->getNumber(); + return $a['identifier']->getNumber() <=> $b['identifier']->getNumber(); }); } @@ -157,11 +210,11 @@ public function sortDesc(): void { $this->uniqueArray->sort(function($a, $b) { // First, compare year descending - if ($a->getYear() !== $b->getYear()) { - return $b->getYear() <=> $a->getYear(); + if ($a['identifier']->getYear() !== $b['identifier']->getYear()) { + return $b['identifier']->getYear() <=> $a['identifier']->getYear(); } // If years are equal, compare ID descending - return $b->getNumber() <=> $a->getNumber(); + return $b['identifier']->getNumber() <=> $a['identifier']->getNumber(); }); } @@ -184,7 +237,7 @@ public function getNewestIdentifier(): CertificateIdentifier { $this->sortDesc(); // get first element of sort descending -> newest element - return $this->uniqueArray->at(0); + return $this->uniqueArray->at(0)['identifier']; } /** @@ -194,10 +247,21 @@ public function getNewestIdentifier(): CertificateIdentifier */ public function toStr(): string { - $return_str = "Certificate Identifiers:\n"; - foreach ($this->uniqueArray->toArray() as $identifier) { - $return_str .= $identifier->toStr() . "\n"; + $returnStr = "Certificate Identifiers:\n"; + foreach ($this->uniqueArray->toArray() as $identifierInformation) { + $returnStr .= $identifierInformation['identifier']->toStr() . "\n"; } - return $return_str; + return $returnStr; + } + + public function getIssueInformationByIdentifier(CertificateIdentifier $identifier): ?array + { + foreach ($this->uniqueArray->toArray() as $identifierInformation) { + if($identifierInformation['identifier']->toStr() == $identifier->toStr()){ + return $identifierInformation; + } + } + + return null; } } \ No newline at end of file diff --git a/classes/CodecheckRegister/CodecheckGithubRegisterApiClient.php b/classes/CodecheckRegister/CodecheckGithubRegisterApiClient.php index 85c50d16..8ab7cc6f 100644 --- a/classes/CodecheckRegister/CodecheckGithubRegisterApiClient.php +++ b/classes/CodecheckRegister/CodecheckGithubRegisterApiClient.php @@ -12,6 +12,9 @@ use APP\plugins\generic\codecheck\classes\Exceptions\ApiFetchException; use APP\plugins\generic\codecheck\classes\Exceptions\ApiCreateException; use APP\plugins\generic\codecheck\classes\Exceptions\GithubUrlParseException; +use APP\plugins\generic\codecheck\classes\CodecheckRegister\CodecheckGithubRegisterIssue; +use APP\plugins\generic\codecheck\classes\Exceptions\ApiUpdateException; +use APP\plugins\generic\codecheck\classes\Log\CodecheckLogger; // Load .env variables $dotenv = Dotenv::createImmutable(__DIR__ . '/../../'); @@ -83,9 +86,9 @@ public static function parseGithubUrl(string $url): array } /** - * Fetches all Issues from the CODECHECK GitHub Register + * Fetches only the first newest Issues from the CODECHECK GitHub Register */ - public function fetchIssues(): void + public function fetchNewestIssues(): void { $issuePage = 1; $issuesToFetchPerPage = 20; @@ -106,7 +109,7 @@ public function fetchIssues(): void } // stop looping if no more issues exist and we haven't yet found a matching issue - if (empty($allissues) && empty($this->issue)) { + if (empty($allissues) && empty($this->issues)) { throw new NoMatchingIssuesFoundException("There was no open or closed issue found with the label 'id assigned' in the GitHub Codecheck Register."); } @@ -121,6 +124,54 @@ public function fetchIssues(): void } while (!$fetchedMatchingIssue); } + /** + * Fetches all Issues from the CODECHECK GitHub Register + */ + public function fetchAllIssues(): void + { + try { + $allissues = $this->client->api('search')->issues('repo:' . $this->githubRegisterOrganization . '/' . $this->githubRegisterRepository . ' sort:"updated"'); + } catch (\Throwable $e) { + throw new ApiFetchException("Failed fetching the GitHub Issues\n" . $e->getMessage()); + } + + foreach ($allissues['items'] as $issue) { + if (strpos($issue['title'], '|') !== false) { + $this->issues[] = $issue; + } + } + + // stop if no issues exist and we haven't yet found any matching issue + if (empty($allissues) && empty($this->issues)) { + throw new NoMatchingIssuesFoundException("There was no open or closed issue found with the label 'id assigned' in the GitHub Codecheck Register."); + } + } + + /** + * Fetches all Issues from the CODECHECK GitHub Register + */ + public function fetchIssueByIdentifier( + CertificateIdentifier $certificateIdentifier + ): void + { + try { + $allissues = $this->client->api('search')->issues('repo:' . $this->githubRegisterOrganization . '/' . $this->githubRegisterRepository . ' "'. $certificateIdentifier->toStr() . '" sort:"updated"'); + } catch (\Throwable $e) { + throw new ApiFetchException("Failed fetching the GitHub Issues\n" . $e->getMessage()); + } + + foreach ($allissues['items'] as $issue) { + if (strpos($issue['title'], '|') !== false) { + $this->issues[] = $issue; + } + } + + // stop if no issues exist and we haven't yet found any matching issue + if (empty($allissues) && empty($this->issues)) { + throw new NoMatchingIssuesFoundException("There was no open or closed issue found with the label 'id assigned' in the GitHub Codecheck Register."); + } + } + /** * Fetches a Issue Labels from the CODECHECK GitHub Register */ @@ -141,45 +192,108 @@ public function fetchLabels(): void * Adds an Issue with the new Certificate Identifier to the CODECHECK GitHub Register * * @param CertificateIdentifier $certificateIdentifier The Certificate identifier to be added - * @param string $codecheckVenueType The CODECHECK Venue Type that will be added as a label to the issue - * @param string $codecheckVenueName The CODECHECK Venue Name that will be added as a second label to the issue + * @param CodecheckIssueLabels $codecheckIssueLabels The CODECHECK Issue Labels that will be added * @param string $authorString The formatted author string e.g. `author name et al.` - * @return string Returns the GitHub URL of the newly created issue + * @param string $paperTitle The Title of the submitted paper / preprint / article + * @return array Returns the GitHub URL & Issue Number of the newly created issue */ public function addIssue( CertificateIdentifier $certificateIdentifier, - string $codecheckVenueType, - string $codecheckVenueName, - array $customLabels, - ?string $authorString, - ): string { + CodecheckIssueLabels $codecheckIssueLabels, + string $paperTitle, + string $authorString, + array $codecheckers, + array $repositories + ): array { $this->client->authenticate($this->githubPAT, null, Client::AUTH_ACCESS_TOKEN); - $authorString = empty($authorString) ? 'New CODECHECK' : $authorString; - $issueTitle = $authorString . ' | ' . $certificateIdentifier->toStr(); - $issueBody = 'Journal: `' . $this->journalName . '`
' . 'Submission ID: `' . $this->submissionID . '`'; - $labelStrings = ['id assigned']; + $codecheckIssue = new CodecheckGithubRegisterIssue( + $this->githubRegisterOrganization, + $this->githubRegisterRepository, + $certificateIdentifier, + $codecheckIssueLabels, + $paperTitle, + $this->journalName, + $authorString, + $this->submissionID, + $codecheckers, + $repositories + ); - $labelStrings[] = $codecheckVenueType; - $labelStrings[] = $codecheckVenueName; + try { + $issue = $this->client->api('issue')->create( + $this->githubRegisterOrganization, + $this->githubRegisterRepository, + [ + 'title' => $codecheckIssue->getTitle(), + 'body' => $codecheckIssue->getBody(), + 'labels' => $codecheckIssue->getLabels() + ] + ); + } catch (\Throwable $e) { + throw new ApiCreateException("Error while adding the new GitHub issue with the new Certificate Identifier: " . $certificateIdentifier->toStr() . "\n" . $e->getMessage()); + } - $labelStrings = array_merge($labelStrings, $customLabels); + return $issue; + } - //error_log(print_r($labelStrings, true)); - error_log($this->githubRegisterOrganization); + /** + * Adds an Issue with the new Certificate Identifier to the CODECHECK GitHub Register + * + * @param int $issueNumber The Number of the corresponding GitHub Issue + * @param CertificateIdentifier $certificateIdentifier The Certificate identifier to be added + * @param CodecheckIssueLabels $codecheckIssueLabels The CODECHECK Issue Labels that will be updated + * @param string $authorString The formatted author string e.g. `author name et al.` + * @param string $paperTitle The Title of the submitted paper / preprint / article + * @return array Returns the GitHub URL & Issue Number of the newly created issue + */ + public function updateIssue( + int $issueNumber, + CertificateIdentifier $certificateIdentifier, + CodecheckIssueLabels $codecheckIssueLabels, + string $paperTitle, + string $authorString, + array $codecheckers, + array $repositories + ): array { + $token = $_ENV['CODECHECK_REGISTER_GITHUB_TOKEN']; + + $this->client->authenticate($token, null, Client::AUTH_ACCESS_TOKEN); + + $codecheckIssue = new CodecheckGithubRegisterIssue( + $this->githubRegisterOrganization, + $this->githubRegisterRepository, + $certificateIdentifier, + $codecheckIssueLabels, + $paperTitle, + $this->journalName, + $authorString, + $this->submissionID, + $codecheckers, + $repositories + ); + + $issueContents = [ + 'title' => $codecheckIssue->getTitle(), + 'body' => $codecheckIssue->getBody() + ]; + + if(!empty($codecheckIssueLabels->get()->toArray())){ + $issueContents['labels'] = $codecheckIssue->getLabels(); + } try { - $issue = $this->client->api('issue')->create($this->githubRegisterOrganization, $this->githubRegisterRepository, [ - 'title' => $issueTitle, - 'body' => $issueBody, - 'labels' => $labelStrings - ] + $issue = $this->client->api('issue')->update( + $this->githubRegisterOrganization, + $this->githubRegisterRepository, + $issueNumber, + $issueContents, ); } catch (\Throwable $e) { - throw new ApiCreateException("Error while adding the new GitHub issue with the new Certificate Identifier: " . $certificateIdentifier->toStr() . "\n" . $e->getMessage()); + throw new ApiUpdateException("Error while updating GitHub issue #$issueNumber with the Certificate Identifier: " . $certificateIdentifier->toStr() . "\n" . $e->getMessage()); } - return $issue['html_url']; + return $issue; } /** diff --git a/classes/CodecheckRegister/CodecheckGithubRegisterIssue.php b/classes/CodecheckRegister/CodecheckGithubRegisterIssue.php new file mode 100644 index 00000000..3e8578d9 --- /dev/null +++ b/classes/CodecheckRegister/CodecheckGithubRegisterIssue.php @@ -0,0 +1,142 @@ +repositoryOwner = $repositoryOwner; + $this->repository = $repository; + $this->submissionID = $submissionID; + $authorString = empty($authorString) ? 'New CODECHECK' : $authorString; + $this->title = $this->createTitleMarkdown($authorString, $certificateIdentifier); + $this->jsonEncodedCodecheckMetadata = $this->createJsonEncodedCodecheckMetadataMarkdown($authorString, $certificateIdentifier, $journalName, $submissionID, $codecheckers, $repositories); + $this->body = $this->createBodyMarkdown($paperTitle, $journalName, $repositories) . "\n" . $this->jsonEncodedCodecheckMetadata; + $this->labels = $this->fillLabels($codecheckIssueLabels); + } + + public function getRepositoryOwner(): string + { + return $this->repositoryOwner; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getBody(): string + { + return $this->body; + } + + public function getLabels(): array + { + return $this->labels; + } + + private function createTitleMarkdown( + string $authorString, + CertificateIdentifier $certificateIdentifier + ): string + { + return $authorString . ' | ' . $certificateIdentifier->toStr(); + } + + private function createJsonEncodedCodecheckMetadataMarkdown( + string $authorString, + CertificateIdentifier $certificateIdentifier, + string $journalName, + string $submissionID, + array $codecheckers, + array $repositories + ): string + { + return "
\n

JSON encoded CODECHECK metadata

\n\n" + . "```json\n" + . "{" + . "\n\t\"identifier\": \"" . $certificateIdentifier->toStr() . "\"," + . "\n\t\"repositories\": " . json_encode($repositories) . "," + . "\n\t\"codecheckers\": " . json_encode($codecheckers) . "," + . "\n\t\"links\": []," + . "\n\t\"journal\": {\"name\": \"" . $journalName . "\", \"submissionID\": $submissionID}," + . "\n}" + . "\n```" + . "\n\n
"; + } + + private function createBodyMarkdown( + string $paperTitle, + string $journalName, + array $repositories + ): string + { + $repoStr = ""; + foreach ($repositories as $repo) { + $repoStr .= "\t- " . $repo . "\n"; + } + return "\n## " . $paperTitle . "\n\n" + . "\n**Article:**\n\n" + . "\n**Journal:** " . $journalName . " *(Submission ID: " . $this->submissionID . ")*\n\n" + . "\n**Repositories:**\n" + . $repoStr; + } + + private function fillLabels( + CodecheckIssueLabels $codecheckIssueLabels + ): array + { + $labels = ['id assigned']; + $labels = array_merge($labels, $codecheckIssueLabels->get()->toArray()); + + return $labels; + } + + private function getFormattedLabelsForUrl(): string + { + $labels = ""; + $countLabels = 0; + foreach($this->labels as $label) { + $labels = $labels . rawurlencode($label); + + if($countLabels < count($this->labels) - 1) { + $labels = $labels . ","; + } + + $countLabels++; + } + + return $labels; + } + + public function getNewIssueUrl(): string + { + $url = "https://github.com/$this->repositoryOwner/$this->repository/issues/new"; + $queryTitle = "title=" . rawurlencode($this->title); + $queryBody = "body=" . rawurlencode($this->body); + $queryLabels = "labels=" . $this->getFormattedLabelsForUrl(); + + return $url . "?" . $queryTitle . "&" . $queryBody . "&" . $queryLabels; + } +} \ No newline at end of file diff --git a/classes/CodecheckRegister/CodecheckIssueLabels.php b/classes/CodecheckRegister/CodecheckIssueLabels.php new file mode 100644 index 00000000..76b720ed --- /dev/null +++ b/classes/CodecheckRegister/CodecheckIssueLabels.php @@ -0,0 +1,144 @@ +uniqueArray = UniqueArray::from($issueLabelArray); + } + + public static function fromApi(string $url, ?CodecheckApiClient $apiClient = null): CodecheckIssueLabels + { + $issueLabelArray = []; + + // fetch CODECHECK Certificate GitHub Labels + // Intialize API caller + $codecheckApiClient = $apiClient ?? new CodecheckApiClient(); + // fetch CODECHECK Type data + try { + $codecheckApiClient->fetch($url); + } catch (CurlInitException $curlInitException) { + // TODO: Implement that the user gets notified, that the fetching of the Labels didn't work + CodecheckLogger::error('CurlInit Exception: ' . $curlInitException->getMessage()); + throw $curlInitException; + } catch (CurlReadException $curlReadException) { + // TODO: Implement that the user gets notified, that the fetching of the Labels didn't work + CodecheckLogger::error('CurlRead Exception: ' . $curlReadException->getMessage()); + throw $curlReadException; + } + // get json Data from API call + $data = $codecheckApiClient->getData(); + + foreach($data as $venue) { + $label = $venue["Issue label"]; + // If a Label is "id assigned" or "development" it automatically gets assigned + // Therefore this Label has to be skipped here, as it shouldn't be selected manually by the user + if($label == "id assigned" || $label == "development") { + continue; + } + // add Label to Venue Names + $issueLabelArray[] = $label; + } + + $codecheckIssueLabels = new CodecheckIssueLabels($issueLabelArray); + + $codecheckIssueLabels->saveIssueLabelsToDB(); + + return $codecheckIssueLabels; + } + + public static function fromDB(): CodecheckIssueLabels + { + $issueLabelRecords = DB::table('codecheck_issue_labels') + ->pluck('label') + ->toArray(); + + CodecheckLogger::debug("Issue Label Records: " . json_encode($issueLabelRecords)); + + $codecheckIssueLabels = new CodecheckIssueLabels($issueLabelRecords ?? []); + return $codecheckIssueLabels; + } + + /** + * This function saves all the Codecheck Issue Labels to the Database + * + * @return void + */ + public function saveIssueLabelsToDB(): bool + { + CodecheckLogger::debug("Saving Issue Label data to DB: " . print_r($this->uniqueArray->toArray(), true)); + + $tableName = 'codecheck_issue_labels'; + + $tableExists = Schema::hasTable('codecheck_issue_labels'); + + if(!$tableExists) { + CodecheckLogger::debug("Issue Label Table doesnt exist"); + return !$tableExists; + } + + $labelsLastUpdated = date('Y-m-d H:i:s'); + + foreach ($this->uniqueArray->toArray() as $label) { + if(is_string($label)) { + $dbLabelRecord = [ + 'label' => $label, + 'labels_last_updated' => $labelsLastUpdated + ]; + + $recordExists = DB::table($tableName) + ->where('label', $label) + ->exists(); + + if ($recordExists) { + DB::table($tableName) + ->where('label', $label) + ->update($dbLabelRecord); + CodecheckLogger::debug("Updated existing label record"); + } else { + DB::table($tableName)->insert($dbLabelRecord); + CodecheckLogger::debug("Created new label record"); + } + } + } + + return true; + } + + public function add(string $issue): void + { + $this->uniqueArray->add($issue); + } + + public function addLabelArray(array $labels): void + { + foreach ($labels as $label) { + $this->uniqueArray->add($label); + } + } + + /** + * Gets the List of all CODECHECK Venue Names + * + * @return UniqueArray Returns all CODECHECK Venue Names inside a `UniqueArray` + */ + public function get(): UniqueArray + { + return $this->uniqueArray; + } +} diff --git a/classes/CodecheckRegister/CodecheckVenue.php b/classes/CodecheckRegister/CodecheckVenue.php deleted file mode 100644 index 8cd4f668..00000000 --- a/classes/CodecheckRegister/CodecheckVenue.php +++ /dev/null @@ -1,61 +0,0 @@ -setVenueType($venueType); - $this->setVenueName($venueName); - } - - /** - * Sets the name of the CODECHECK Venue - * - * @param string $venueName The new Venue Name to be set - */ - public function setVenueName(string $venueName) - { - $this->venueName = str_replace(["\r", "\n"], "", $venueName); - } - - /** - * Sets the type of the CODECHECK Venue - * - * @param string $venueType The new Venue Type to be set - */ - public function setVenueType(string $venueType) - { - $this->venueType = str_replace(["\r", "\n"], "", $venueType); - } - - /** - * Gets the name of the CODECHECK Venue - * - * @return string Name of the CODECHECK Venue - */ - public function getVenueName(): string - { - return $this->venueName; - } - - /** - * Gets the type of the CODECHECK Venue - * - * @return string Type of the CODECHECK Venue - */ - public function getVenueType(): string - { - return $this->venueType; - } -} \ No newline at end of file diff --git a/classes/CodecheckRegister/CodecheckVenueNames.php b/classes/CodecheckRegister/CodecheckVenueNames.php deleted file mode 100644 index cd91eba8..00000000 --- a/classes/CodecheckRegister/CodecheckVenueNames.php +++ /dev/null @@ -1,66 +0,0 @@ -uniqueArray = new UniqueArray(); - - // fetch CODECHECK Certificate GitHub Labels - // Intialize API caller - $codecheckApiClient = $apiClient ?? new CodecheckApiClient(); - // fetch CODECHECK Type data - try { - $codecheckApiClient->fetch("https://codecheck.org.uk/register/venues/index.json"); - } catch (CurlInitException $curlInitException) { - // TODO: Implement that the user gets notified, that the fetching of the Labels didn't work - CodecheckLogger::error('CurlInit Exception: ' . $curlInitException->getMessage()); - throw $curlInitException; - } catch (CurlReadException $curlReadException) { - // TODO: Implement that the user gets notified, that the fetching of the Labels didn't work - CodecheckLogger::error('CurlRead Exception: ' . $curlReadException->getMessage()); - throw $curlReadException; - } - // get json Data from API call - $data = $codecheckApiClient->getData(); - - // find all venue Types - // TODO: Remove this once the actualy Codecheck API contains the labels/ Venue Names to fetch - $codecheckVenueTypes = $codecheckVenueTypes ?? new CodecheckVenueTypes(); - - foreach($data as $venue) { - $label = $venue["Issue label"]; - // If a Label is already a Venue Type it can't also be a venue Name - // Therefore this Label has to be skipped - if($codecheckVenueTypes->get()->contains($label) || $label == "id assigned" || $label == "development") { - continue; - } - // add Label to Venue Names - $this->uniqueArray->add($label); - } - } - - /** - * Gets the List of all CODECHECK Venue Names - * - * @return UniqueArray Returns all CODECHECK Venue Names inside a `UniqueArray` - */ - public function get(): UniqueArray - { - return $this->uniqueArray; - } -} diff --git a/classes/CodecheckRegister/CodecheckVenueTypes.php b/classes/CodecheckRegister/CodecheckVenueTypes.php deleted file mode 100644 index b1999311..00000000 --- a/classes/CodecheckRegister/CodecheckVenueTypes.php +++ /dev/null @@ -1,55 +0,0 @@ -uniqueArray = new UniqueArray(); - // Intialize API caller - $codecheckApiClient = $apiClient ?? new CodecheckApiClient(); - // fetch CODECHECK Type data - try { - $codecheckApiClient->fetch("https://codecheck.org.uk/register/venues/index.json"); - } catch (CurlInitException $curlInitException) { - // TODO: Implement that the user gets notified, that the fetching of the Labels didn't work - CodecheckLogger::error($curlInitException->getMessage()); - throw $curlInitException; - } catch (CurlReadException $curlReadException) { - // TODO: Implement that the user gets notified, that the fetching of the Labels didn't work - CodecheckLogger::error($curlReadException->getMessage()); - throw $curlReadException; - } - // get json Data from API Caller - $data = $codecheckApiClient->getData(); - - foreach($data as $venue) { - // insert every type (as this is a unique Array each Type will only occur once) - $type = $venue["Venue type"]; - // Add every venue type to the unique Array - $this->uniqueArray->add($type); - } - } - - /** - * Gets the List of all CODECHECK Venue Types - * - * @return UniqueArray Returns all CODECHECK Venue Types inside a `UniqueArray` - */ - public function get(): UniqueArray - { - return $this->uniqueArray; - } -} \ No newline at end of file diff --git a/classes/DataStructures/UniqueArray.php b/classes/DataStructures/UniqueArray.php index a573ded3..f9b14e77 100644 --- a/classes/DataStructures/UniqueArray.php +++ b/classes/DataStructures/UniqueArray.php @@ -68,6 +68,21 @@ public function contains(mixed $element): bool ); } + /** + * This function checks if a specific Certificate Identifier exists inside the `UniqueArray` + * + * @return bool Returns `true` if the identifier exists inside the `UniqueArray` and `false` if otherwise + */ + public function containsIdentifier(array $element): bool + { + foreach ($this->array as $uniqueArrayElement) { + if($element["identifier"]->toStr() == $uniqueArrayElement['identifier']->toStr()){ + return true; + } + } + return false; + } + /** * This function converts the `UniqueArray` to a normal array and returns this * diff --git a/classes/Exceptions/ApiUpdateException.php b/classes/Exceptions/ApiUpdateException.php new file mode 100644 index 00000000..053ce666 --- /dev/null +++ b/classes/Exceptions/ApiUpdateException.php @@ -0,0 +1,14 @@ + $data['source'] ?? '', 'codecheckers' => isset($data['codecheckers']) ? json_encode($data['codecheckers']) : null, 'certificate' => $data['certificate'] ?? '', + 'issueUrl' => $data['issueUrl'] ?? '', + 'issueNumber' => $data['issueNumber'] ?? null, 'check_time' => $data['check_time'] ?? null, 'summary' => $data['summary'] ?? '', 'report' => $data['report'] ?? '', @@ -115,6 +117,16 @@ public function getCertificate(): string return $this->data['certificate'] ?? ''; } + public function getIssueUrl(): string + { + return $this->data['issueUrl'] ?? ''; + } + + public function getIssueNumber(): string + { + return $this->data['issueNumber'] ?? ''; + } + public function getCheckTime(): ?string { return $this->data['check_time'] ?? null; diff --git a/classes/Workflow/CodecheckMetadataHandler.php b/classes/Workflow/CodecheckMetadataHandler.php index 50109071..ac091343 100644 --- a/classes/Workflow/CodecheckMetadataHandler.php +++ b/classes/Workflow/CodecheckMetadataHandler.php @@ -85,6 +85,7 @@ public function getMetadata($request, $submissionId): array 'codecheckers' => json_decode($metadata->codecheckers ?? '[]', true), 'source' => $metadata->source, 'certificate' => $metadata->certificate, + 'issue' => json_decode($metadata->issue ?? '[]', true), 'check_time' => $metadata->check_time, 'summary' => $metadata->summary, 'report' => $metadata->report, @@ -119,6 +120,7 @@ public function saveMetadata($request, $submissionId): array 'source' => $nullIfEmpty($data['source'] ?? null), 'codecheckers' => json_encode($data['codecheckers'] ?? []), 'certificate' => $nullIfEmpty($data['certificate'] ?? null), + 'issue' => json_encode($data['issue'] ?? ['url' => null, 'number' => null, 'labelsSelected' => []]), 'check_time' => $nullIfEmpty($data['check_time'] ?? null), 'summary' => $nullIfEmpty($data['summary'] ?? null), 'report' => $nullIfEmpty($data['report'] ?? null), diff --git a/classes/migration/CodecheckSchemaMigration.php b/classes/migration/CodecheckSchemaMigration.php index fda97123..bfd40141 100644 --- a/classes/migration/CodecheckSchemaMigration.php +++ b/classes/migration/CodecheckSchemaMigration.php @@ -1,9 +1,11 @@ text('source')->nullable(); $table->text('codecheckers')->nullable(); $table->string('certificate', 100)->nullable(); + $table->string('issue', 500)->default(json_encode(['url' => null, 'number' => null, 'labelsSelected' => []])); $table->timestamp('check_time')->nullable(); $table->text('summary')->nullable(); $table->string('report', 500)->nullable(); @@ -31,6 +34,17 @@ public function up(): void $this->createCodecheckGenres(); } + public function issueLabelsUp(): void + { + if(!Schema::hasTable('codecheck_issue_labels')) { + CodecheckLogger::debug("Creating Issue Label DB Schema"); + Schema::create('codecheck_issue_labels', function (Blueprint $table) { + $table->string('label', 200)->default(''); + $table->string('labels_last_updated', 100)->default(date('Y-m-d H:i:s')); + }); + } + } + private function createCodecheckGenres(): void { $contextDao = \APP\core\Application::getContextDAO(); @@ -65,4 +79,9 @@ public function down(): void { Schema::dropIfExists('codecheck_metadata'); } + + public function issueLabelsDown(): void + { + Schema::dropIfExists('codecheck_issue_labels'); + } } \ No newline at end of file diff --git a/cypress.config.js b/cypress.config.js index 54c08c69..cb76176b 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -11,7 +11,15 @@ export default defineConfig({ }, specPattern: 'cypress/tests/component/**/*.cy.js', supportFile: 'cypress/support/component.js', - indexHtmlFile: 'cypress/support/component-index.html' + indexHtmlFile: 'cypress/support/component-index.html', + setupNodeEvents(on, config) { + on('task', { + log(message) { + console.log(message); + return null; + } + }); + } }, e2e: { // Default: local OJS at port 8888 (common dev setup) diff --git a/cypress/tests/component/CodecheckMetadataForm.cy.js b/cypress/tests/component/CodecheckMetadataForm.cy.js index 206c7f2c..2aacbfae 100644 --- a/cypress/tests/component/CodecheckMetadataForm.cy.js +++ b/cypress/tests/component/CodecheckMetadataForm.cy.js @@ -33,21 +33,25 @@ describe('CodecheckMetadataForm Component', () => { check_time: '', summary: '', report: '', - additionalContent: '' - } + additionalContent: '', + issue: { + url: "https://github.come/example/repo/issues/0", + number: 0, + labels: ["test-label-1", "test-label-2"], + labelsSelected: ["test-label-2"] + }, + }, } }).as('loadMetadata'); - // Mock venue data API - cy.intercept('GET', '**/codecheck/venue', { + cy.intercept('GET', '**/codecheck/labels*', { statusCode: 200, body: { success: true, - message: 'Venue data loaded', - venueTypes: ['Journal', 'Conference', 'Preprint'], - venueNames: ['Nature', 'Science', 'PLOS ONE', 'arXiv'] + labels: ['test-label-1', 'test-label-2'], + message: 'Labels fetched successfully' } - }).as('getVenueData'); + }).as('loadLabelData'); }); it('renders loading state initially', () => { @@ -70,7 +74,7 @@ describe('CodecheckMetadataForm Component', () => { }); cy.wait('@loadMetadata'); - + // Check paper metadata section cy.contains('Test Article Title').should('exist'); cy.contains('John Doe').should('exist'); @@ -305,13 +309,13 @@ describe('CodecheckMetadataForm Component', () => { }); cy.wait('@loadMetadata'); - cy.wait('@getVenueData'); - - cy.get('.certificate-identifier-venue-types option') - .should('have.length.gt', 1); + cy.wait('@loadLabelData'); - cy.get('.certificate-identifier-venue-names option') - .should('have.length.gt', 1); + cy.get('.certificate-identifier-select.dropdown .dropdown-checkbox-input') + .should('have.length.gt', 0); + + cy.get('.certificate-identifier-select.dropdown .dropdown-checkbox-input input[type="checkbox"]') + .should('have.length', 2); }); it('can reserve certificate identifier', () => { @@ -329,7 +333,8 @@ describe('CodecheckMetadataForm Component', () => { body: { success: true, identifier: '2025-042', - issueUrl: 'https://github.com/codecheckers/register/issues/42' + issueUrl: 'https://github.com/codecheckers/register/issues/42', + issueNumber: 42 } }).as('reserveIdentifier'); @@ -341,23 +346,24 @@ describe('CodecheckMetadataForm Component', () => { }); cy.wait('@loadMetadata'); - cy.wait('@getVenueData'); - - // Select venue type and name - cy.get('.certificate-identifier-venue-types').select('Journal'); - cy.get('.certificate-identifier-venue-names').select('Nature'); + cy.wait('@loadLabelData'); - // Click reserve button - cy.get('#certificate-identifier-button-wrapper') - .find('button') - .contains(/reserve/i) - .click(); - - cy.on('window:alert', (text) => { - expect(text).to.contains('2025-042'); + // Open dropdown and select a label first, otherwise the guard blocks the request + cy.get('.dropdown-content').invoke('show'); + cy.get('.dropdown-content').should('be.visible'); + cy.get('.dropdown-checkbox-input input[type="checkbox"]').first().check(); + + cy.get('.certificate-identifier-button').contains( + 'plugins.generic.codecheck.identifier.reserve.withApi' + ).click(); + + cy.wait('@reserveIdentifier').then((interception) => { + expect(interception.request.body).to.have.property('reserveIdentifierMode', 'api'); + expect(interception.request.body.labels).to.have.length.gt(0); }); - - cy.wait('@reserveIdentifier'); + + cy.get('.certificate-identifier-input') + .should('have.value', '2025-042'); }); it('disables preview button when requirements not met', () => { diff --git a/locale/en/locale.po b/locale/en/locale.po index 60ff0c95..02572155 100644 --- a/locale/en/locale.po +++ b/locale/en/locale.po @@ -269,8 +269,29 @@ msgstr "View GitHub Issue" msgid "plugins.generic.codecheck.identifier.save" msgstr "Save Identifier" -msgid "plugins.generic.codecheck.identifier.reserve" -msgstr "Reserve Identifier" +msgid "plugins.generic.codecheck.identifier.update.error.message" +msgstr "Error while updating the GitHub Issue" + +msgid "plugins.generic.codecheck.identifier.reserve.linkExistingIdentifier" +msgstr "Link Existing Identifier" + +msgid "plugins.generic.codecheck.identifier.reserve.withApi" +msgstr "Reserve Identifier Automatically" + +msgid "plugins.generic.codecheck.identifier.reserve.withNewIssueUrl" +msgstr "Reserve Identifier Manually" + +msgid "plugins.generic.codecheck.identifier.reserve.withNewIssueUrl.success.message" +msgstr "Success: You are being forwarded to GitHub. Please finish the creation of the Issue there!" + +msgid "plugins.generic.codecheck.identifier.reserve.withNewIssueUrl.fail.message" +msgstr "Error while creating the New Issue URL" + +msgid "plugins.generic.codecheck.identifier.reserve.linkExistingIdentifier.success.message" +msgstr "The GitHub Issue was linked to OJS with the Certificate Identifier" + +msgid "plugins.generic.codecheck.identifier.reserve.linkExistingIdentifier.fail.message" +msgstr "Error while linking an existing GitHub Issue" msgid "plugins.generic.codecheck.identifier.reserve.success.message" msgstr "You have reserved the Certificate Identifier" @@ -293,6 +314,9 @@ msgstr "Do you want to reserve a new identifier for this CODECHECK?" msgid "plugins.generic.codecheck.identifier.removeConfirm" msgstr "Are you sure you want to remove this identifier?" +msgid "plugins.generic.codecheck.identifier.labels" +msgstr "GitHub Labels" + msgid "plugins.generic.codecheck.previewYaml" msgstr "Preview CODECHECK metadata file" @@ -428,7 +452,10 @@ msgid "plugins.generic.codecheck.validation.codecheckersRequired" msgstr "Please add at least one codechecker" msgid "plugins.generic.codecheck.validation.certificateRequired" -msgstr "Please provide a certificate ID" +msgstr "Please reserve/ input a Certificate Identifier" + +msgid "plugins.generic.codecheck.validation.githubIssueLinkRequired" +msgstr "Please link the OJS form to the corresponding GitHub Issue with the same Certificate Identifier" msgid "plugins.generic.codecheck.validation.summaryRequired" msgstr "Please provide a summary" diff --git a/public/build/build.iife.js b/public/build/build.iife.js index 69e4412d..8b1b6b08 100644 --- a/public/build/build.iife.js +++ b/public/build/build.iife.js @@ -1,56 +1,62 @@ -(function(e){"use strict";const _=(c,i)=>{const n=c.__vccOpts||c;for(const[o,t]of i)n[o]=t;return n},C={class:"codecheck-manifest-files"},D={class:"manifest-files-list"},B=["onUpdate:modelValue","placeholder"],T=["onUpdate:modelValue","placeholder"],M=["onClick"],w=_({__name:"CodecheckManifestFiles",props:{name:{type:String,required:!0},label:{type:String,required:!0},description:{type:String,default:""},value:{type:String,default:""},isRequired:{type:Boolean,default:!1}},setup(c){const{useLocalize:i}=pkp.modules.useLocalize,{t:n}=i(),o=c,t=e.ref([]);e.onMounted(()=>{o.value&&o.value.split(` -`).forEach(l=>{var m,p;if(l.trim()){const r=l.split(" - ");t.value.push({filename:((m=r[0])==null?void 0:m.trim())||"",comment:((p=r[1])==null?void 0:p.trim())||""})}}),t.value.length===0&&a()});function a(){t.value.push({filename:"",comment:""})}function s(l){t.value.splice(l,1),t.value.length===0&&a(),d()}function d(){var r;const l=t.value.filter(h=>h.filename.trim()).map(h=>{const u=h.filename.trim(),g=h.comment.trim();return g?`${u} - ${g}`:u}).join(` -`),m=new CustomEvent("update",{detail:l,bubbles:!0}),p=(r=document.querySelector(`textarea[name="${o.name}"]`))==null?void 0:r.previousElementSibling;p&&p.dispatchEvent(m)}return(l,m)=>(e.openBlock(),e.createElementBlock("div",C,[e.createElementVNode("div",D,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(t.value,(p,r)=>(e.openBlock(),e.createElementBlock("div",{key:r,class:"manifest-file-row"},[e.withDirectives(e.createElementVNode("input",{"onUpdate:modelValue":h=>p.filename=h,type:"text",placeholder:e.unref(n)("plugins.generic.codecheck.manifestFiles.filenamePlaceholder"),onInput:d,class:"form-control"},null,40,B),[[e.vModelText,p.filename]]),e.withDirectives(e.createElementVNode("input",{"onUpdate:modelValue":h=>p.comment=h,type:"text",placeholder:e.unref(n)("plugins.generic.codecheck.manifestFiles.commentPlaceholder"),onInput:d,class:"form-control"},null,40,T),[[e.vModelText,p.comment]]),e.createElementVNode("button",{type:"button",onClick:h=>s(r),class:"btn-remove"},"×",8,M)]))),128))]),e.createElementVNode("button",{type:"button",onClick:a,class:"btn-add"},e.toDisplayString(e.unref(n)("plugins.generic.codecheck.manifestFiles.addButton")),1)]))}},[["__scopeId","data-v-b2e2483c"]]),x={class:"codecheck-repository-list"},I={class:"repository-list"},R=["onUpdate:modelValue","placeholder","onBlur"],F=["onClick"],E=_({__name:"CodecheckRepositoryList",props:{name:{type:String,required:!0},label:{type:String,required:!0},description:{type:String,default:""},value:{type:String,default:""}},setup(c){const{useLocalize:i}=pkp.modules.useLocalize,{t:n}=i(),o=c,t=e.ref([]),a=e.ref([]);e.onMounted(()=>{o.value&&o.value.split(` -`).forEach(p=>{p.trim()&&t.value.push(p.trim())}),t.value.length===0&&s()});function s(){t.value.push("https://"),a.value.push("")}function d(p){t.value.splice(p,1),a.value.splice(p,1),m()}function l(p){const r=t.value[p];if(!r.trim()||r==="https://"){a.value[p]="";return}try{new URL(r),!r.startsWith("http://")&&!r.startsWith("https://")?a.value[p]=n("plugins.generic.codecheck.repository.validation.protocol"):a.value[p]=""}catch{a.value[p]=n("plugins.generic.codecheck.repository.validation.invalid")}}function m(){var u;const p=t.value.filter(g=>g.trim()&&g!=="https://").join(` -`),r=new CustomEvent("update",{detail:p,bubbles:!0}),h=(u=document.querySelector(`textarea[name="${o.name}"]`))==null?void 0:u.previousElementSibling;h&&h.dispatchEvent(r)}return(p,r)=>(e.openBlock(),e.createElementBlock("div",x,[e.createElementVNode("div",I,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(t.value,(h,u)=>(e.openBlock(),e.createElementBlock("div",{key:u,class:"repository-row"},[e.withDirectives(e.createElementVNode("input",{"onUpdate:modelValue":g=>t.value[u]=g,type:"url",placeholder:e.unref(n)("plugins.generic.codecheck.repository.placeholder"),onInput:m,onBlur:g=>l(u),class:e.normalizeClass(["form-control",{"is-invalid":a.value[u]}])},null,42,R),[[e.vModelText,t.value[u]]]),e.createElementVNode("button",{type:"button",onClick:g=>d(u),class:"btn-remove"},"×",8,F)]))),128)),p.error?(e.openBlock(!0),e.createElementBlock(e.Fragment,{key:0},e.renderList(a.value,(h,u)=>(e.openBlock(),e.createElementBlock("div",{key:"error-"+u,class:"pkpFormField__error"},e.toDisplayString(h),1))),128)):e.createCommentVNode("",!0)]),e.createElementVNode("button",{type:"button",onClick:s,class:"btn-add"},e.toDisplayString(e.unref(n)("plugins.generic.codecheck.repository.addButton")),1)]))}},[["__scopeId","data-v-7edbd363"]]),U={class:"codecheck-review-display"},L={key:0,class:"codecheck-info"},v={class:"info-section"},A={key:0,class:"info-section"},O={key:1,class:"info-section"},P={key:2,class:"info-section"},z={key:3,class:"info-section"},H={key:0},q={key:4,class:"info-section"},j={key:0,class:"orcid-badge"},Y={key:5,class:"info-section"},W=["href"],K={key:6,class:"info-section"},J={key:7,class:"info-section"},X={key:8,class:"info-section"},G=["href"],Q={class:"actions"},Z={key:1,class:"codecheck-not-opted"},$=_({__name:"CodecheckReviewDisplay",props:{submission:{type:Object,required:!0}},setup(c){const{useLocalize:i}=pkp.modules.useLocalize,{t:n}=i(),o=c,t=e.computed(()=>{if(o.submission.codecheckMetadata){if(typeof o.submission.codecheckMetadata=="string")try{return JSON.parse(o.submission.codecheckMetadata)}catch(r){return console.error("Failed to parse codecheck metadata:",r),{}}return o.submission.codecheckMetadata}return{}}),a=e.computed(()=>Object.keys(t.value).length>0);function s(){return t.value.certificate&&t.value.checkTime?"complete":a.value?"in-progress":"pending"}const d=e.computed(()=>{switch(s()){case"complete":return"status-complete";case"in-progress":return"status-in-progress";case"pending":default:return"status-pending"}});function l(){switch(s()){case"complete":return n("plugins.generic.codecheck.status.complete");case"in-progress":return n("plugins.generic.codecheck.status.inProgress");case"pending":default:return n("plugins.generic.codecheck.status.pending")}}function m(r){return r?new Date(r).toLocaleString():""}function p(){const r=pkp.registry.getPiniaStore("workflow");r.selectedMenuState={primaryMenuItem:"workflow",stageId:999}}return(r,h)=>{var g;const u=e.resolveComponent("pkp-button");return e.openBlock(),e.createElementBlock("div",U,[e.createElementVNode("h3",null,e.toDisplayString(e.unref(n)("plugins.generic.codecheck.reviewTitle")),1),(g=c.submission)!=null&&g.codecheckOptIn?(e.openBlock(),e.createElementBlock("div",L,[e.createElementVNode("div",v,[e.createElementVNode("h4",null,e.toDisplayString(e.unref(n)("plugins.generic.codecheck.status")),1),e.createElementVNode("span",{class:e.normalizeClass(["status-badge",d.value])},e.toDisplayString(l()),3)]),a.value&&t.value.configVersion?(e.openBlock(),e.createElementBlock("div",A,[e.createElementVNode("h4",null,e.toDisplayString(e.unref(n)("plugins.generic.codecheck.review.configVersion")),1),e.createElementVNode("p",null,e.toDisplayString(t.value.configVersion),1)])):e.createCommentVNode("",!0),a.value&&t.value.publicationType?(e.openBlock(),e.createElementBlock("div",O,[e.createElementVNode("h4",null,e.toDisplayString(e.unref(n)("plugins.generic.codecheck.review.publicationType")),1),e.createElementVNode("p",null,e.toDisplayString(t.value.publicationType==="doi"?e.unref(n)("plugins.generic.codecheck.review.publicationType.doi"):e.unref(n)("plugins.generic.codecheck.review.publicationType.separate")),1)])):e.createCommentVNode("",!0),a.value&&t.value.certificate?(e.openBlock(),e.createElementBlock("div",P,[e.createElementVNode("h4",null,e.toDisplayString(e.unref(n)("plugins.generic.codecheck.identifier.label")),1),e.createElementVNode("p",null,e.toDisplayString(t.value.certificate),1)])):e.createCommentVNode("",!0),a.value&&t.value.manifest&&t.value.manifest.length>0?(e.openBlock(),e.createElementBlock("div",z,[e.createElementVNode("h4",null,e.toDisplayString(e.unref(n)("plugins.generic.codecheck.review.manifestFiles")),1),e.createElementVNode("ul",null,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(t.value.manifest,(y,b)=>(e.openBlock(),e.createElementBlock("li",{key:b},[e.createElementVNode("strong",null,e.toDisplayString(y.file),1),y.comment?(e.openBlock(),e.createElementBlock("span",H," - "+e.toDisplayString(y.comment),1)):e.createCommentVNode("",!0)]))),128))])])):e.createCommentVNode("",!0),a.value&&t.value.codecheckers&&t.value.codecheckers.length>0?(e.openBlock(),e.createElementBlock("div",q,[e.createElementVNode("h4",null,e.toDisplayString(e.unref(n)("plugins.generic.codecheck.review.codecheckers")),1),e.createElementVNode("ul",null,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(t.value.codecheckers,(y,b)=>(e.openBlock(),e.createElementBlock("li",{key:b},[e.createTextVNode(e.toDisplayString(y.name)+" ",1),y.orcid?(e.openBlock(),e.createElementBlock("span",j,e.toDisplayString(y.orcid),1)):e.createCommentVNode("",!0)]))),128))])])):e.createCommentVNode("",!0),a.value&&t.value.repository?(e.openBlock(),e.createElementBlock("div",Y,[e.createElementVNode("h4",null,e.toDisplayString(e.unref(n)("plugins.generic.codecheck.repositories.title")),1),e.createElementVNode("a",{href:t.value.repository,target:"_blank"},e.toDisplayString(t.value.repository),9,W)])):e.createCommentVNode("",!0),a.value&&t.value.checkTime?(e.openBlock(),e.createElementBlock("div",K,[e.createElementVNode("h4",null,e.toDisplayString(e.unref(n)("plugins.generic.codecheck.completionTime.label")),1),e.createElementVNode("p",null,e.toDisplayString(m(t.value.checkTime)),1)])):e.createCommentVNode("",!0),a.value&&t.value.summary?(e.openBlock(),e.createElementBlock("div",J,[e.createElementVNode("h4",null,e.toDisplayString(e.unref(n)("plugins.generic.codecheck.certificate.summary")),1),e.createElementVNode("p",null,e.toDisplayString(t.value.summary),1)])):e.createCommentVNode("",!0),a.value&&t.value.reportUrl?(e.openBlock(),e.createElementBlock("div",X,[e.createElementVNode("h4",null,e.toDisplayString(e.unref(n)("plugins.generic.codecheck.review.reportUrl")),1),e.createElementVNode("a",{href:t.value.reportUrl,target:"_blank"},e.toDisplayString(t.value.reportUrl),9,G)])):e.createCommentVNode("",!0),e.createElementVNode("div",Q,[e.createVNode(u,{onClick:p},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(n)("plugins.generic.codecheck.viewFullMetadata")),1)]),_:1})])])):(e.openBlock(),e.createElementBlock("div",Z,[e.createElementVNode("p",null,e.toDisplayString(e.unref(n)("plugins.generic.codecheck.notOptedIn")),1)]))])}}},[["__scopeId","data-v-2ac9f582"]]),{useLocalize:ee}=pkp.modules.useLocalize,te={name:"CodecheckMetadataForm",props:{submission:{type:Object,required:!0},canEdit:{type:Boolean,default:!0},name:{type:String},value:{type:String}},setup(){const{t:c}=ee();return{t:c}},data(){return{loading:!0,saving:!1,dataLoaded:!1,error:null,saveMessage:"",saveMessageType:"",repositories:[],hasUnsavedChanges:!1,submissionData:{id:null,title:"",authors:[],doi:"",codeRepository:"",dataRepository:"",manifestFiles:"",dataAvailabilityStatement:""},certificateIdentifier:{venueType:"default",venueName:"default",venueTypes:[],venueNames:[],customLabelSelected:[],customLabels:[],issueUrl:""},metadata:{version:"latest",publicationType:"doi",manifest:[],repository:"",source:"",codecheckers:[],certificate:"",check_time:"",summary:"",report:"",additionalContent:""}}},computed:{canPreview(){return this.metadata.manifest.length>0&&this.metadata.codecheckers.length>0&&this.metadata.certificate&&!this.hasUnsavedChanges},previewButtonTooltip(){return this.hasUnsavedChanges?this.t("plugins.generic.codecheck.preview.saveFirst"):this.canPreview?this.t("plugins.generic.codecheck.preview.ready"):this.t("plugins.generic.codecheck.preview.missingRequired")},isIdentifierReserved(){return this.metadata.certificate.trim()!==""}},mounted(){this.loadData(),this.getVenueData()},watch:{metadata:{handler(){this.hasUnsavedChanges=!0},deep:!0},repositories:{handler(){this.hasUnsavedChanges=!0},deep:!0}},methods:{async loadData(){var c,i,n,o,t,a,s,d;this.loading=!0,this.error=null,this.dataLoaded=!1;try{if(!this.submission||!this.submission.id)throw new Error("Invalid submission object");const l=this.submission.id;let m=pkp.context.apiBaseUrl;m+="codecheck",m=`${m}/metadata?submissionId=${l}`;const p=await fetch(m,{method:"GET",headers:{"X-Csrf-Token":pkp.currentUser.csrfToken}}),r=await p.json();if(!p.ok||!r.success)throw new Error(`[HTTP ${p.status}] ${r.error}`);this.submissionData={id:((c=r.submission)==null?void 0:c.id)||l,title:((i=r.submission)==null?void 0:i.title)||"",authors:Array.isArray((n=r.submission)==null?void 0:n.authors)?r.submission.authors:[],doi:((o=r.submission)==null?void 0:o.doi)||"",codeRepository:((t=r.submission)==null?void 0:t.codeRepository)||"",dataRepository:((a=r.submission)==null?void 0:a.dataRepository)||"",manifestFiles:((s=r.submission)==null?void 0:s.manifestFiles)||"",dataAvailabilityStatement:((d=r.submission)==null?void 0:d.dataAvailabilityStatement)||""},r.codecheck&&typeof r.codecheck=="object"&&(this.metadata={version:r.codecheck.version||r.codecheck.version||"latest",publicationType:r.codecheck.publicationType||r.codecheck.publication_type||"doi",manifest:Array.isArray(r.codecheck.manifest)?r.codecheck.manifest:typeof r.codecheck.manifest=="string"?JSON.parse(r.codecheck.manifest):[],repository:r.codecheck.repository||"",source:r.codecheck.source||"",codecheckers:Array.isArray(r.codecheck.codecheckers)?r.codecheck.codecheckers:typeof r.codecheck.codecheckers=="string"?JSON.parse(r.codecheck.codecheckers):[],certificate:r.codecheck.certificate||"",check_time:r.codecheck.check_time||r.codecheck.check_time?this.formatDateTimeLocal(r.codecheck.check_time||r.codecheck.check_time):"",summary:r.codecheck.summary||"",report:r.codecheck.report||r.codecheck.report||"",additionalContent:r.codecheck.additionalContent||r.codecheck.additional_content||""},r.codecheck.repository&&(this.repositories=r.codecheck.repository.split(",").map(h=>h.trim()).filter(h=>h))),this.dataLoaded=!0,this.$nextTick(()=>{this.hasUnsavedChanges=!1})}catch(l){console.error("Load error:",l),this.error=this.t("plugins.generic.codecheck.loadError")+": "+l.message}finally{this.loading=!1}},async loadMetadataFromRepository(c){var o,t,a,s,d,l,m,p,r,h,u,g,y,b;let i=this.repositories[c];console.log(i);let n=pkp.context.apiBaseUrl+"codecheck";try{const f=await(await fetch(`${n}/repository`,{method:"POST",headers:{"Content-Type":"application/json","X-Csrf-Token":pkp.currentUser.csrfToken},body:JSON.stringify({repository:i})})).json();f.success?(console.log("Success:",f.repository),this.submissionData={id:this.submissionData.id,title:((o=f.metadata)==null?void 0:o.paper.title)??this.submissionData.title,authors:((t=f.metadata)==null?void 0:t.paper.authors)??this.submissionData.authors,doi:((a=f.metadata)==null?void 0:a.paper.doi)??this.submissionData.doi,codeRepository:this.submissionData.codeRepository,dataRepository:this.submissionData.dataRepository,manifestFiles:((s=f.metadata)==null?void 0:s.manifest)??this.submissionData.manifestFiles,dataAvailabilityStatement:this.submissionData.dataAvailabilityStatement},this.metadata={version:((d=f.metadata)==null?void 0:d.version.replace(/^https:\/\/codecheck\.org\.uk\/spec\/config\/|\/$/g,""))??this.metadata.version,publicationType:((l=f.metadata)==null?void 0:l.publicationType)??this.metadata.publicationType,manifest:((m=f.metadata)==null?void 0:m.manifest)??this.metadata.manifest,repository:this.metadata.repository,source:((p=f.metadata)==null?void 0:p.source)??this.metadata.source,codecheckers:((r=f.metadata)==null?void 0:r.codechecker)??this.metadata.codecheckers,certificate:((h=f.metadata)==null?void 0:h.certificate)??this.metadata.certificate,check_time:this.formatDateTimeLocal((u=f.metadata)==null?void 0:u.check_time)??this.metadata.check_time,summary:((g=f.metadata)==null?void 0:g.summary)??this.metadata.summary,report:((y=f.metadata)==null?void 0:y.report)??this.metadata.report,additionalContent:((b=f.metadata)==null?void 0:b.additionalContent)??this.metadata.additionalContent}):console.error("Error:",f.error)}catch(N){console.error("Failed to fetch metadata from existing Repository:",N)}},triggerFileUpload(){this.$refs.fileInput.click()},handleFileUpload(c){const i=c.target.files;if(!(!i||i.length===0)){for(let n=0;n";i({title:this.t("plugins.generic.codecheck.repositories.infoTitle"),message:n,actions:[{label:this.t("plugins.generic.codecheck.modal.close"),callback:o=>o()}]})},showFallbackRepositoryInfoModal(){prompt(this.t("plugins.generic.codecheck.repositories.infoTextOne")+this.t("plugins.generic.codecheck.repositories.infoTextTwo")+this.t("plugins.generic.codecheck.repositories.infoTextNote")+this.t("plugins.generic.codecheck.repositories.infoTextMoreInformation")+''+this.t("plugins.generic.codecheck.repositories.infoTextLinkToAllowedRepositories")+"")},showCodecheckerModal(){this.canUsePkpModal()?this.showPkpCodecheckerModal():this.showFallbackCodecheckerModal()},showPkpCodecheckerModal(){const{useModal:c}=pkp.modules.useModal,{openDialog:i}=c(),n='';i({title:this.t("plugins.generic.codecheck.codecheckers.addCodechecker"),message:n,actions:[{label:this.t("plugins.generic.codecheck.modal.cancel"),callback:o=>o()},{label:this.t("plugins.generic.codecheck.modal.add"),isPrimary:!0,callback:o=>{const t=document.getElementById("checker-name"),a=document.getElementById("checker-orcid"),s=(t==null?void 0:t.value)||"",d=(a==null?void 0:a.value)||"";s.trim()&&this.metadata.codecheckers.push({name:s.trim(),orcid:d.trim()}),o()}}]})},showFallbackCodecheckerModal(){const c=prompt(this.t("plugins.generic.codecheck.codecheckers.enterName"));if(c&&c.trim()){const i=prompt(this.t("plugins.generic.codecheck.codecheckers.enterOrcid"));this.metadata.codecheckers.push({name:c.trim(),orcid:i?i.trim():""})}},removeCodechecker(c){confirm(this.t("plugins.generic.codecheck.codecheckers.removeConfirm"))&&this.metadata.codecheckers.splice(c,1)},async saveMetadata(){if(this.validateForm()){this.saving=!0,this.saveMessage="";try{const c={version:this.metadata.version,publication_type:this.metadata.publicationType,manifest:this.metadata.manifest,repository:this.repositories.join(", "),source:this.metadata.source,codecheckers:this.metadata.codecheckers,certificate:this.metadata.certificate,check_time:this.metadata.check_time,summary:this.metadata.summary,report:this.metadata.report,additional_content:this.metadata.additionalContent};console.log("Saving CODECHECK data:",c);const i=this.submission.id;let n=pkp.context.apiBaseUrl;n+="codecheck",n=`${n}/metadata?submissionId=${i}`;const o=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json","X-Csrf-Token":pkp.currentUser.csrfToken},body:JSON.stringify(c)}),t=await o.json();if(!o.ok||!t.success)throw new Error(`[HTTP ${o.status}] ${t.error}`);this.hasUnsavedChanges=!1,this.showMessage(this.t("plugins.generic.codecheck.savedSuccessfully"),"success")}catch(c){console.error("Save error:",c),this.showMessage(this.t("plugins.generic.codecheck.saveFailed")+": "+c.message,"error")}finally{this.saving=!1}}},async generateYamlContent(){try{const c=this.submission.id;let i=pkp.context.apiBaseUrl;i+="codecheck",i=`${i}/yaml?submissionId=${c}`;const n=await fetch(i,{method:"GET",headers:{"X-Csrf-Token":pkp.currentUser.csrfToken}});if(!n.ok)throw new Error("Failed to generate YAML");return(await n.json()).yaml}catch(c){throw console.error("Yaml generation error:",c),c}},async previewYaml(){try{const c=await this.generateYamlContent();if(!await this.validateGeneratedYamlFile(c))return;this.canUsePkpModal()?this.showYamlModal(c):this.showYamlFallback(c)}catch(c){console.error("Preview error:",c),this.showMessage(`${this.t("plugins.generic.codecheck.yamlPreviewFailed")} -${c}`,"error")}},showYamlModal(c){const{useModal:i}=pkp.modules.useModal,{openDialog:n}=i(),o="downloadCodecheckYaml_"+Date.now();window[o]=function(){const a=new Blob([c],{type:"text/yaml"}),s=URL.createObjectURL(a),d=document.createElement("a");d.href=s,d.download="codecheck.yml",d.click(),URL.revokeObjectURL(s)};const t='
'+this.escapeHtml(c)+"
";n({title:this.t("plugins.generic.codecheck.yaml.previewTitle"),message:t,actions:[{label:this.t("plugins.generic.codecheck.yaml.download"),isPrimary:!0,callback:a=>{window[o](),delete window[o],a()}},{label:this.t("plugins.generic.codecheck.yaml.close"),callback:a=>{delete window[o],a()}}]})},showYamlFallback(c){const i=window.open("","_blank"),n=this.escapeHtml(c),o=JSON.stringify(c),t='CODECHECK Metadata Preview

📄 CODECHECK Metadata Preview

'+n+"