From e83c2f72e65418cc30635d297450b8612a35e159 Mon Sep 17 00:00:00 2001 From: Laetitia Fesselier Date: Tue, 29 Jun 2021 15:07:03 -0400 Subject: [PATCH 1/7] [API] Subproject endpoint --- .../api/php/endpoints/candidates.class.inc | 4 +- .../endpoints/subproject/subproject.class.inc | 133 ++++++++++++++++ .../api/php/endpoints/subprojects.class.inc | 149 ++++++++++++++++++ modules/api/php/module.class.inc | 3 + modules/api/php/views/subproject.class.inc | 91 +++++++++++ modules/api/php/views/subprojects.class.inc | 54 +++++++ 6 files changed, 432 insertions(+), 2 deletions(-) create mode 100644 modules/api/php/endpoints/subproject/subproject.class.inc create mode 100644 modules/api/php/endpoints/subprojects.class.inc create mode 100644 modules/api/php/views/subproject.class.inc create mode 100644 modules/api/php/views/subprojects.class.inc diff --git a/modules/api/php/endpoints/candidates.class.inc b/modules/api/php/endpoints/candidates.class.inc index a1dedbc78ae..6550fb34422 100644 --- a/modules/api/php/endpoints/candidates.class.inc +++ b/modules/api/php/endpoints/candidates.class.inc @@ -211,12 +211,12 @@ class Candidates extends Endpoint implements \LORIS\Middleware\ETagCalculator $candid = \Candidate::createNew( new \CenterID("$centerid"), $data['Candidate']['DoB'], - $data['Candidate']['EDC'], + $data['Candidate']['EDC'] ?? null, $sex, $pscid, $project->getId() ); - } catch (\LorisException $e) { + } catch (\LorisException | \InvalidArgumentException | \Exception $e) { return new \LORIS\Http\Response\JSON\BadRequest($e->getMessage()); } diff --git a/modules/api/php/endpoints/subproject/subproject.class.inc b/modules/api/php/endpoints/subproject/subproject.class.inc new file mode 100644 index 00000000000..ab3e2e6c51a --- /dev/null +++ b/modules/api/php/endpoints/subproject/subproject.class.inc @@ -0,0 +1,133 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://github.com/aces/Loris + */ +class Subproject extends Endpoint implements \LORIS\Middleware\ETagCalculator +{ + /** + * A cache of the results of the projects/$projectname endpoint, so that + * it doesn't need to be recalculated for the ETag and handler + */ + private $_cache; + + /** + * The requested project + */ + private $_subproject; + + /** + * Contructor + * + * @param string $subproject The requested project + */ + public function __construct(string $subproject) + { + $this->_subproject = $subproject; + } + + /** + * Return which methods are supported by this endpoint. + * + * @return array supported HTTP methods + */ + protected function allowedMethods() : array + { + return ['GET']; + } + + /** + * Versions of the LORIS API which are supported by this + * endpoint. + * + * @return array a list of supported API versions. + */ + protected function supportedVersions() : array + { + return [ + 'v0.0.4-dev', + ]; + } + + /** + * Handles a request that starts with /projects/$projectname + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface The outgoing PSR7 response + */ + public function handle(ServerRequestInterface $request) : ResponseInterface + { + $pathparts = $request->getAttribute('pathparts'); + if (count($pathparts) === 0) { + switch ($request->getMethod()) { + case 'GET': + return $this->_handleGET($request); + + case 'OPTIONS': + return (new \LORIS\Http\Response()) + ->withHeader('Allow', $this->allowedMethods()); + + default: + return new \LORIS\Http\Response\JSON\MethodNotAllowed( + $this->allowedMethods() + ); + } + } + return new \LORIS\Http\Response\JSON\NotFound(); + } + + /** + * Generates a JSON representation of this project following the API + * specification. + * + * @param ServerRequestInterface $request The incoming PSR7 request @unused-param + * + * @return ResponseInterface + */ + private function _handleGET($request): ResponseInterface + { + if (isset($this->_cache)) { + return $this->_cache; + } + + $view = (new \LORIS\api\Views\SubProject($this->_subproject)) + ->toArray(); + $this->_cache = new \LORIS\Http\Response\JsonResponse($view); + + return $this->_cache; + } + + /** + * Implements the ETagCalculator interface + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return string etag summarizing value of this request. + */ + public function ETag($request) : string + { + return md5(json_encode($this->_handleGET($request)->getBody())); + } +} diff --git a/modules/api/php/endpoints/subprojects.class.inc b/modules/api/php/endpoints/subprojects.class.inc new file mode 100644 index 00000000000..8347a1a4253 --- /dev/null +++ b/modules/api/php/endpoints/subprojects.class.inc @@ -0,0 +1,149 @@ +getAttribute('user'); + if (!$this->_hasAccess($user)) { + return new \LORIS\Http\Response\JSON\Unauthorized(); + } + + $pathparts = $request->getAttribute('pathparts'); + + if (count($pathparts) === 1) { + switch ($request->getMethod()) { + case 'GET': + return $this->_handleGET($request); + + case 'OPTIONS': + return (new \LORIS\Http\Response()) + ->withHeader('Allow', $this->allowedMethods()); + default: + return new \LORIS\Http\Response\JSON\MethodNotAllowed( + $this->allowedMethods() + ); + } + } + + // Delegate to subproject specific endpoint. + $subproject_name = $pathparts[1] ?? ''; + if (!in_array( + $subproject_name, + array_values(\Utility::getSubprojectList()) + ) + ) { + return new \LORIS\Http\Response\JSON\NotFound(); + } + + $endpoint = new Subproject\Subproject($subproject_name); + // Removing `/subprojects/` from pathparts. + $pathparts = array_slice($pathparts, 2); + $request = $request->withAttribute('pathparts', $pathparts); + + return $endpoint->process($request, $endpoint); + } + + /** + * Returns a list of subprojects for this LORIS instance. + * + * @return ResponseInterface + */ + private function _handleGET() : ResponseInterface + { + if (!isset($this->_cache)) { + // The subprojects list should be related to the user in the request + // attributes. + $DB = \NDB_Factory::singleton()->database(); + $subprojects = $DB->pselect("SELECT * FROM subproject", []); + $this->_cache = (new \LORIS\api\Views\Subprojects($subprojects)) + ->toArray(); + } + + return new \LORIS\Http\Response\JsonResponse( + $this->_cache + ); + } + + /** + * Implements the ETagCalculator interface + * + * @param ServerRequestInterface $request The PSR7 incoming request. + * + * @return string etag summarizing value of this request. + */ + public function ETag(ServerRequestInterface $request) : string + { + return md5(json_encode($this->_handleGet($request)->getBody())); + } +} diff --git a/modules/api/php/module.class.inc b/modules/api/php/module.class.inc index 50df8a4c3b6..2a71a3e8df6 100644 --- a/modules/api/php/module.class.inc +++ b/modules/api/php/module.class.inc @@ -109,6 +109,9 @@ class Module extends \Module case 'projects': $handler = new \LORIS\api\Endpoints\Projects(); break; + case 'subprojects': + $handler = new \LORIS\api\Endpoints\Subprojects(); + break; default: return new \LORIS\Http\Response\JSON\NotFound(); } diff --git a/modules/api/php/views/subproject.class.inc b/modules/api/php/views/subproject.class.inc new file mode 100644 index 00000000000..7ab4dae9147 --- /dev/null +++ b/modules/api/php/views/subproject.class.inc @@ -0,0 +1,91 @@ +_subproject = $subproject; + } + + /** + * Produce an array representation of this project. + * + * @return array + */ + public function toArray(): array + { + $meta = ['Subproject' => $this->_subproject]; + + return [ + 'Meta' => $meta, + 'Visits' => $this->getVisits(), + ]; + } + + /** + * Generates the list of valid visit_labels for the subproject + * as defined in the config settings + * + * @return array + */ + protected function getVisits(): array + { + $visits = []; + + $subprojectID = array_search( + $this->_subproject, + \Utility::getSubprojectList() + ); + + $config =& \NDB_Config::singleton(); + $visitLabelSettings = $config->getSetting('visitLabel'); + foreach (\Utility::associativeToNumericArray($visitLabelSettings) + as $visitLabelSetting) { + if ($visitLabelSetting['@']['subprojectID'] == $subprojectID) { + + $items = \Utility::associativeToNumericArray( + $visitLabelSetting['labelSet']['item'] + ); + foreach ($items as $item) { + $visits[] = $item['@']['value']; + } + break; + } + } + + return $visits; + } +} + diff --git a/modules/api/php/views/subprojects.class.inc b/modules/api/php/views/subprojects.class.inc new file mode 100644 index 00000000000..815384d6590 --- /dev/null +++ b/modules/api/php/views/subprojects.class.inc @@ -0,0 +1,54 @@ +_subprojects = $subprojects; + } + + /** + * Gets the required data from the projects. + * + * @return array + */ + public function toArray(): array + { + $subprojects = []; + foreach ($this->_subprojects as $subproject) { + $subprojects[$subproject['title']] = [ + 'useEDC' => $subproject['useEDC'], + 'WindowDifference' => $subproject['WindowDifference'], + 'RecruitmentTarget' => $subproject['RecruitmentTarget'] + ]; + } + return ['Subprojects' => $subprojects]; + } +} From 1909443480ec0c823245d80ee83195f605fb967a Mon Sep 17 00:00:00 2001 From: Laetitia Fesselier Date: Mon, 9 Aug 2021 19:49:13 -0400 Subject: [PATCH 2/7] Typos and urldecode fix --- .../php/endpoints/subproject/subproject.class.inc | 12 ++++++------ modules/api/php/endpoints/subprojects.class.inc | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/api/php/endpoints/subproject/subproject.class.inc b/modules/api/php/endpoints/subproject/subproject.class.inc index ab3e2e6c51a..3fc351ffd59 100644 --- a/modules/api/php/endpoints/subproject/subproject.class.inc +++ b/modules/api/php/endpoints/subproject/subproject.class.inc @@ -16,7 +16,7 @@ use \Psr\Http\Message\ResponseInterface; use \LORIS\api\Endpoint; /** - * A class for handling the /projects/$projectname endpoint. + * A class for handling the /subprojects/$subprojectname endpoint. * * @category API * @package Loris @@ -27,20 +27,20 @@ use \LORIS\api\Endpoint; class Subproject extends Endpoint implements \LORIS\Middleware\ETagCalculator { /** - * A cache of the results of the projects/$projectname endpoint, so that + * A cache of the results of the subprojects/$subprojectname endpoint, so that * it doesn't need to be recalculated for the ETag and handler */ private $_cache; /** - * The requested project + * The requested subproject */ private $_subproject; /** * Contructor * - * @param string $subproject The requested project + * @param string $subproject The requested subproject */ public function __construct(string $subproject) { @@ -71,7 +71,7 @@ class Subproject extends Endpoint implements \LORIS\Middleware\ETagCalculator } /** - * Handles a request that starts with /projects/$projectname + * Handles a request that starts with /subprojects/$subprojectname * * @param ServerRequestInterface $request The incoming PSR7 request * @@ -99,7 +99,7 @@ class Subproject extends Endpoint implements \LORIS\Middleware\ETagCalculator } /** - * Generates a JSON representation of this project following the API + * Generates a JSON representation of this subproject following the API * specification. * * @param ServerRequestInterface $request The incoming PSR7 request @unused-param diff --git a/modules/api/php/endpoints/subprojects.class.inc b/modules/api/php/endpoints/subprojects.class.inc index 8347a1a4253..1de27aa5109 100644 --- a/modules/api/php/endpoints/subprojects.class.inc +++ b/modules/api/php/endpoints/subprojects.class.inc @@ -97,7 +97,7 @@ class Subprojects extends Endpoint implements \LORIS\Middleware\ETagCalculator } // Delegate to subproject specific endpoint. - $subproject_name = $pathparts[1] ?? ''; + $subproject_name = urldecode($pathparts[1] ?? ''); if (!in_array( $subproject_name, array_values(\Utility::getSubprojectList()) From 00e5d5fc2b4c06268a0f84fb5a4af6ab6fc6249d Mon Sep 17 00:00:00 2001 From: xlecours Date: Wed, 20 Oct 2021 15:48:46 -0400 Subject: [PATCH 3/7] adding AccessibleResource interface to project adding getSubproject to project moving /subproject endpoint under project --- .../php/endpoints/project/project.class.inc | 3 + modules/api/php/endpoints/projects.class.inc | 4 + .../api/php/endpoints/subprojects.class.inc | 149 ------------------ modules/api/php/module.class.inc | 3 - php/libraries/Project.class.inc | 53 ++++++- php/libraries/Subproject.class.inc | 111 +++++++++++++ 6 files changed, 170 insertions(+), 153 deletions(-) delete mode 100644 modules/api/php/endpoints/subprojects.class.inc create mode 100644 php/libraries/Subproject.class.inc diff --git a/modules/api/php/endpoints/project/project.class.inc b/modules/api/php/endpoints/project/project.class.inc index c732da3cc51..40480795db0 100644 --- a/modules/api/php/endpoints/project/project.class.inc +++ b/modules/api/php/endpoints/project/project.class.inc @@ -110,6 +110,9 @@ class Project extends Endpoint implements \LORIS\Middleware\ETagCalculator case 'instruments': $handler = new Instruments($this->_project); break; + case 'subprojects': + $handler = new Subprojects($this->_project); + break; case 'visits': $handler = new Visits($this->_project); break; diff --git a/modules/api/php/endpoints/projects.class.inc b/modules/api/php/endpoints/projects.class.inc index 584cc7ab432..670882a2c0c 100644 --- a/modules/api/php/endpoints/projects.class.inc +++ b/modules/api/php/endpoints/projects.class.inc @@ -108,6 +108,10 @@ class Projects extends Endpoint implements \LORIS\Middleware\ETagCalculator return new \LORIS\Http\Response\JSON\NotFound(); } + if (!$project->isAccessibleBy($user)) { + return new \LORIS\Http\Response\JSON\Forbidden(); + } + $endpoint = new Project\Project($project); // Removing `/projects/` from pathparts. diff --git a/modules/api/php/endpoints/subprojects.class.inc b/modules/api/php/endpoints/subprojects.class.inc deleted file mode 100644 index 1de27aa5109..00000000000 --- a/modules/api/php/endpoints/subprojects.class.inc +++ /dev/null @@ -1,149 +0,0 @@ -getAttribute('user'); - if (!$this->_hasAccess($user)) { - return new \LORIS\Http\Response\JSON\Unauthorized(); - } - - $pathparts = $request->getAttribute('pathparts'); - - if (count($pathparts) === 1) { - switch ($request->getMethod()) { - case 'GET': - return $this->_handleGET($request); - - case 'OPTIONS': - return (new \LORIS\Http\Response()) - ->withHeader('Allow', $this->allowedMethods()); - default: - return new \LORIS\Http\Response\JSON\MethodNotAllowed( - $this->allowedMethods() - ); - } - } - - // Delegate to subproject specific endpoint. - $subproject_name = urldecode($pathparts[1] ?? ''); - if (!in_array( - $subproject_name, - array_values(\Utility::getSubprojectList()) - ) - ) { - return new \LORIS\Http\Response\JSON\NotFound(); - } - - $endpoint = new Subproject\Subproject($subproject_name); - // Removing `/subprojects/` from pathparts. - $pathparts = array_slice($pathparts, 2); - $request = $request->withAttribute('pathparts', $pathparts); - - return $endpoint->process($request, $endpoint); - } - - /** - * Returns a list of subprojects for this LORIS instance. - * - * @return ResponseInterface - */ - private function _handleGET() : ResponseInterface - { - if (!isset($this->_cache)) { - // The subprojects list should be related to the user in the request - // attributes. - $DB = \NDB_Factory::singleton()->database(); - $subprojects = $DB->pselect("SELECT * FROM subproject", []); - $this->_cache = (new \LORIS\api\Views\Subprojects($subprojects)) - ->toArray(); - } - - return new \LORIS\Http\Response\JsonResponse( - $this->_cache - ); - } - - /** - * Implements the ETagCalculator interface - * - * @param ServerRequestInterface $request The PSR7 incoming request. - * - * @return string etag summarizing value of this request. - */ - public function ETag(ServerRequestInterface $request) : string - { - return md5(json_encode($this->_handleGet($request)->getBody())); - } -} diff --git a/modules/api/php/module.class.inc b/modules/api/php/module.class.inc index 2a71a3e8df6..50df8a4c3b6 100644 --- a/modules/api/php/module.class.inc +++ b/modules/api/php/module.class.inc @@ -109,9 +109,6 @@ class Module extends \Module case 'projects': $handler = new \LORIS\api\Endpoints\Projects(); break; - case 'subprojects': - $handler = new \LORIS\api\Endpoints\Subprojects(); - break; default: return new \LORIS\Http\Response\JSON\NotFound(); } diff --git a/php/libraries/Project.class.inc b/php/libraries/Project.class.inc index b030a18b8d9..b5365ba8c3f 100644 --- a/php/libraries/Project.class.inc +++ b/php/libraries/Project.class.inc @@ -12,6 +12,7 @@ * @link https://www.github.com/aces/Loris/ */ +use \LORIS\StudyEntities\AccessibleResource; /** * The Project class encapsulates all details of a project. * @@ -21,7 +22,7 @@ * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 * @link https://www.github.com/aces/Loris/ */ -class Project implements \JsonSerializable +class Project implements \JsonSerializable, AccessibleResource { private static $_instances = []; @@ -383,6 +384,42 @@ class Project implements \JsonSerializable return $subProjectData; } + /** + * Get a specific subproject associated with this project. + * + * @param string $title The subproject title + * + * @return \Subproject + */ + public function getSubproject(string $title): \Subproject + { + $filtered = array_filter( + $this->getSubprojects(), + function ($subproject) use ($title) { + return $subproject['title'] == $title; + } + ); + + if (empty($filtered)) { + throw new \NotFound('There is no matching subproject title'); + } + + if (count($filtered) > 1) { + throw new \LorisException('Multiple subprojects with the same title'); + } + + $subproject = array_shift($filtered); + + return new \Subproject( + $subproject['subprojectId'], + $this->_projectId, + $subproject['title'], + $subproject['useEDC'], + $subproject['windowDifference'], + $subproject['recruitmentTarget'] + ); + } + /** * Get that project's participants * @@ -470,4 +507,18 @@ class Project implements \JsonSerializable { return $this->_projectName; } + + /** + * Implements the AccessibleResource interface. A candidate can + * be accessed by a user if it shares the same registration project + * and site or it has access to any of the timepoints. + * + * @param \User $user The user whose access should be validated. + * + * @return bool + */ + public function isAccessibleBy(\User $user): bool + { + return in_array($this->_projectId, $user->getProjectIDs()); + } } diff --git a/php/libraries/Subproject.class.inc b/php/libraries/Subproject.class.inc new file mode 100644 index 00000000000..077e8190e16 --- /dev/null +++ b/php/libraries/Subproject.class.inc @@ -0,0 +1,111 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ + +/** + * The Project class encapsulates all details of a project. + * + * @category Main + * @package Loris + * @author Xavier Lecours Boucher + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Subproject implements \JsonSerializable +{ + private $_subproject_id; + private $_project_id; + private $_title; + private $_useEDC; + private $_windowDifference; + private $_recruitmentTarget; + + /** + * Contructor + * + * @param string $id The subproject id + * @param \ProjectID $project_id the associated project id + * @param string $title This title + * @param ?string $useEDC useEDC Either "0", "1" or null + * @param ?string $windowDifference Value from the database + * @param ?string $recruitmentTarget A numeric string or null + */ + public function __construct( + string $id, + \ProjectID $project_id, + string $title, + ?string $useEDC = null, + ?string $windowDifference = null, + ?string $recruitmentTarget = null + ) { + $this->_subproject_id = $id; + $this->_project_id = $project_id; + $this->_title = $title; + if (!is_null($useEDC)) { + $this->_useEDC = boolval($useEDC); + } + $this->_windowDifference = $windowDifference; + if (!is_null($recruitmentTarget)) { + $this->_recruitmentTarget = intval($recruitmentTarget); + } + } + + /** + * Gets the visit label for that subproject + * + * @return array An array of string + */ + public function getVisits(): array + { + $factory = \NDB_Factory::singleton(); + $visitsData = $factory->database()->pselect( + " + SELECT + v.VisitName visit_label + FROM visit v + LEFT JOIN visit_project_subproject_rel vpsr + ON (v.VisitID = vpsr.VisitID) + LEFT JOIN project_subproject_rel psr + ON (vpsr.ProjectSubprojectRelID = psr.ProjectSubprojectRelID) + WHERE + psr.SubprojectID = :v_subproject_id AND + psr.ProjectID = :v_project_id + ", + [ + 'v_subproject_id' => $this->_subproject_id, + 'v_project_id' => $this->_project_id + ] + ); + + return $visitsData; + } + + /** + * Specify data which should be serialized to JSON. + * Returns data which can be serialized by json_encode(), which is a value of + * any type other than a resource. + * + * @see https://www.php.net/manual/en/jsonserializable.jsonserialize.php + * @return mixed + */ + public function jsonSerialize() + { + return [ + "SubprojectID" => $this->_subproject_id, + "Title" => $this->_title, + "UseEDC" => $this->_useEDC, + "WindowDifference" => $this->_windowDifference, + "RecruitementTarget" => $this->_recruitmentTarget + ]; + } +} From 0d47081846321cd79f41afb84eac5926771af456 Mon Sep 17 00:00:00 2001 From: xlecours Date: Wed, 20 Oct 2021 16:00:06 -0400 Subject: [PATCH 4/7] adding schema file --- modules/api/static/schema-v0.0.4-dev.yml | 110 +++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 modules/api/static/schema-v0.0.4-dev.yml diff --git a/modules/api/static/schema-v0.0.4-dev.yml b/modules/api/static/schema-v0.0.4-dev.yml new file mode 100644 index 00000000000..861fcae1818 --- /dev/null +++ b/modules/api/static/schema-v0.0.4-dev.yml @@ -0,0 +1,110 @@ +openapi: 3.0.1 +info: + title: LORIS - REST API endpoints + description: The LORIS API uses standard HTTP error codes and the body of any response will either be empty or contain only a JSON object for any request. + contact: + name: LORIS Development Team + url: 'https://github.com/aces/loris' + license: + name: 'GNU Public License, Version 3' + url: 'https://opensource.org/licenses/GPL-3.0' + version: 0.0.4-dev +servers: + - url: /api/v0.0.4-dev +security: + - ApiKeyAuth: [] +paths: + '/projects/{project}/subprojects': + get: + tags: + - Projects + summary: List all subprojects of that project + parameters: + - name: project + in: path + description: The project name + required: true + style: simple + explode: false + schema: + type: string + responses: + '200': + description: An object containing the list of instrument summary descriptors + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectSubprojects' + '/projects/{project}/subprojectss/{subproject}': + get: + tags: + - Projects + summary: Get a subproject descriptor in that project + parameters: + - name: project + in: path + description: The project name + required: true + style: simple + explode: false + schema: + type: string + - name: subproject + in: path + description: The subproject title + required: true + style: simple + explode: false + schema: + type: string + responses: + '200': + description: An JSON representation of the subproject + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectSubproject' +components: + schemas: + ProjectSubprojects: + type: object + properties: + Subprojects: + type: array + items: + $ref: '#/components/schemas/Subproject' + Subproject: + type: object + properties: + SubprojectID: + type: string + Title: + type: string + useEDC: + type: boolean + WindowDifference: + type: string + RecruitmentTarget: + type: integer + ProjectSubproject: + type: object + properties: + SubprojectID: + type: string + Title: + type: string + useEDC: + type: boolean + WindowDifference: + type: string + RecruitmentTarget: + type: integer + Visits: + type: array + items: + type: string + securitySchemes: + ApiKeyAuth: + type: apiKey + name: Authorization + in: header From f44faf6c197410674d84db9f60034101021666ec Mon Sep 17 00:00:00 2001 From: xlecours Date: Thu, 21 Oct 2021 10:32:55 -0400 Subject: [PATCH 5/7] adding the endpoints... --- .gitignore | 2 +- .../project/subproject/subproject.class.inc | 145 ++++++++++++++++ .../endpoints/project/subprojects.class.inc | 160 ++++++++++++++++++ modules/api/static/schema-v0.0.4-dev.yml | 14 +- 4 files changed, 316 insertions(+), 5 deletions(-) create mode 100644 modules/api/php/endpoints/project/subproject/subproject.class.inc create mode 100644 modules/api/php/endpoints/project/subprojects.class.inc diff --git a/.gitignore b/.gitignore index 8cd32a3368f..a57723961dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -project +/project smarty/templates_c .gitmodules project-* diff --git a/modules/api/php/endpoints/project/subproject/subproject.class.inc b/modules/api/php/endpoints/project/subproject/subproject.class.inc new file mode 100644 index 00000000000..83eeee56561 --- /dev/null +++ b/modules/api/php/endpoints/project/subproject/subproject.class.inc @@ -0,0 +1,145 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://github.com/aces/Loris + */ +namespace LORIS\api\Endpoints\Project\Subproject; + +use \Psr\Http\Message\ServerRequestInterface; +use \Psr\Http\Message\ResponseInterface; +use \LORIS\api\Endpoint; + +/** + * A class for handling the /projects/$projectname/subprojects/$subproject + * endpoint. + * + * @category API + * @package Loris + * @author Xavier Lecours Boucher + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://github.com/aces/Loris + */ +class Subproject extends Endpoint implements \LORIS\Middleware\ETagCalculator +{ + /** + * A cache of the results of the endpoint, so that + * it doesn't need to be recalculated for the ETag and handler + */ + private $_cache; + + /** + * The requested instrument + */ + private $_subproject; + + /** + * Contructor for the subproject endpoint + * + * @param \Subproject $subproject The subproject + */ + public function __construct(\Subproject $subproject) + { + $this->_subproject = $subproject; + } + + /** + * Return which methods are supported by this endpoint. + * + * @return array supported HTTP methods + */ + protected function allowedMethods() : array + { + return ['GET']; + } + + /** + * Versions of the LORIS API which are supported by this + * endpoint. + * + * @return array a list of supported API versions. + */ + protected function supportedVersions() : array + { + return [ + 'v0.0.4-dev', + ]; + } + + /** + * Handles a HTTP request + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface The outgoing PSR7 response + */ + public function handle(ServerRequestInterface $request) : ResponseInterface + { + $pathparts = $request->getAttribute('pathparts'); + if (count($pathparts) !== 0) { + return new \LORIS\Http\Response\JSON\NotFound(); + } + + switch ($request->getMethod()) { + case 'GET': + return $this->_handleGET($request); + + case 'OPTIONS': + return (new \LORIS\Http\Response()) + ->withHeader('Allow', $this->allowedMethods()); + default: + return new \LORIS\Http\Response\JSON\MethodNotAllowed( + $this->allowedMethods() + ); + } + } + + /** + * Create an array representation of this endpoint's reponse body + * + * @return ResponseInterface The outgoing PSR7 response + */ + private function _handleGET(): ResponseInterface + { + if (isset($this->_cache)) { + return $this->_cache; + } + + $data = json_decode(json_encode($this->_subproject), true); + $visits = array_reduce( + $this->_subproject->getVisits(), + function ($carry, $item) { + // This is just changing the keys from visit_label to VisitLabel + // It should probably be delegated to a view object. + array_push($carry, ['VisitLabel' => $item['visit_label']]); + return $carry; + }, + [] + ); + + $data['Visits'] = $visits; + + $this->_cache = new \LORIS\Http\Response\JsonResponse($data); + + return $this->_cache; + } + + /** + * Implements the ETagCalculator interface + * + * @param ServerRequestInterface $request The PSR7 incoming request. + * + * @return string etag summarizing value of this request. + */ + public function ETag(ServerRequestInterface $request) : string + { + return md5(json_encode($this->_handleGET($request)->getBody())); + } +} + diff --git a/modules/api/php/endpoints/project/subprojects.class.inc b/modules/api/php/endpoints/project/subprojects.class.inc new file mode 100644 index 00000000000..b43cf5943e0 --- /dev/null +++ b/modules/api/php/endpoints/project/subprojects.class.inc @@ -0,0 +1,160 @@ +_project = $project; + } + + /** + * Return which methods are supported by this endpoint. + * + * @return array supported HTTP methods + */ + protected function allowedMethods() : array + { + return ['GET']; + } + + /** + * Versions of the LORIS API which are supported by this + * endpoint. + * + * @return array a list of supported API versions. + */ + protected function supportedVersions() : array + { + return [ + 'v0.0.4-dev', + ]; + } + + /** + * Handles a HTTP request + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface The outgoing PSR7 response + */ + public function handle(ServerRequestInterface $request) : ResponseInterface + { + $pathparts = $request->getAttribute('pathparts'); + if (count($pathparts) === 0) { + switch ($request->getMethod()) { + case 'GET': + return $this->_handleGET(); + + case 'OPTIONS': + return (new \LORIS\Http\Response()) + ->withHeader('Allow', $this->allowedMethods()); + default: + return new \LORIS\Http\Response\JSON\MethodNotAllowed( + $this->allowedMethods() + ); + } + } + + // Delegate to subproject specific endpoint. + $subproject_name = urldecode($pathparts[0] ?? ''); + + try { + $subproject = $this->_project->getSubproject($subproject_name); + } catch (\NotFound $e) { + return new \LORIS\Http\Response\JSON\NotFound(); + } + + $endpoint = new Subproject\Subproject($subproject); + + // Removing `/subprojects/` from pathparts. + $pathparts = array_slice($pathparts, 2); + $request = $request->withAttribute('pathparts', $pathparts); + + return $endpoint->process($request, $endpoint); + } + + /** + * Generates a JSON representation of this projecti subprojects following the API + * specification. + * + * @return ResponseInterface + */ + private function _handleGET(): ResponseInterface + { + if (isset($this->_cache)) { + return $this->_cache; + } + + $subprojects = array_map( + function ($subproject) { + return [ + 'SubprojectID' => $subproject['subprojectId'], + 'Title' => $subproject['title'], + 'UseEDC' => $subproject['useEDC'], + 'WindowDifference' => $subproject['windowDifference'], + 'RecruitmentTarget' => $subproject['recruitmentTarget'] + ]; + }, + $this->_project->getSubprojects() + ); + + $array = [ + 'Subprojects' => $subprojects + ]; + + $this->_cache = new \LORIS\Http\Response\JsonResponse($array); + + return $this->_cache; + } + /** + * Implements the ETagCalculator interface + * + * @param ServerRequestInterface $request The PSR7 incoming request. + * + * @return string etag summarizing value of this request. + */ + public function ETag(ServerRequestInterface $request) : string + { + return md5(json_encode($this->_handleGet($request)->getBody())); + } +} diff --git a/modules/api/static/schema-v0.0.4-dev.yml b/modules/api/static/schema-v0.0.4-dev.yml index 861fcae1818..2a35ed78050 100644 --- a/modules/api/static/schema-v0.0.4-dev.yml +++ b/modules/api/static/schema-v0.0.4-dev.yml @@ -35,7 +35,7 @@ paths: application/json: schema: $ref: '#/components/schemas/ProjectSubprojects' - '/projects/{project}/subprojectss/{subproject}': + '/projects/{project}/subprojects/{subproject}': get: tags: - Projects @@ -80,7 +80,7 @@ components: type: string Title: type: string - useEDC: + UseEDC: type: boolean WindowDifference: type: string @@ -93,16 +93,22 @@ components: type: string Title: type: string - useEDC: + UseEDC: type: boolean WindowDifference: type: string + enum: + - 'optimal' + - 'battery' RecruitmentTarget: type: integer Visits: type: array items: - type: string + type: object + properties: + VisitLabel: + type: string securitySchemes: ApiKeyAuth: type: apiKey From f539c2cb14a9bb2a207d7a8364d89e81a9522270 Mon Sep 17 00:00:00 2001 From: xlecours Date: Thu, 21 Oct 2021 10:44:32 -0400 Subject: [PATCH 6/7] fix cs --- modules/api/php/endpoints/project/subprojects.class.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/api/php/endpoints/project/subprojects.class.inc b/modules/api/php/endpoints/project/subprojects.class.inc index b43cf5943e0..43fb695826a 100644 --- a/modules/api/php/endpoints/project/subprojects.class.inc +++ b/modules/api/php/endpoints/project/subprojects.class.inc @@ -128,7 +128,7 @@ class Subprojects extends Endpoint implements \LORIS\Middleware\ETagCalculator $subprojects = array_map( function ($subproject) { return [ - 'SubprojectID' => $subproject['subprojectId'], + 'SubprojectID' => $subproject['subprojectId'], 'Title' => $subproject['title'], 'UseEDC' => $subproject['useEDC'], 'WindowDifference' => $subproject['windowDifference'], @@ -138,7 +138,7 @@ class Subprojects extends Endpoint implements \LORIS\Middleware\ETagCalculator $this->_project->getSubprojects() ); - $array = [ + $array = [ 'Subprojects' => $subprojects ]; From 725893260b726b518bfe6410b8b6d7330d3a8992 Mon Sep 17 00:00:00 2001 From: xlecours Date: Thu, 21 Oct 2021 15:42:20 -0400 Subject: [PATCH 7/7] now based on Subproject endpoint PR#7502 reshaped the response of /projects/{project} to provide links to subendpoints instead of full list remove the visit list of projects because it depend on subproject added to schema-v0.0.4-dev.yml address the polymorphism issue --- modules/api/php/views/project.class.inc | 8 ++-- .../api/php/views/project_0_0_4_dev.class.inc | 39 +++++++++++++----- modules/api/static/schema-v0.0.4-dev.yml | 41 +++++++++++++++++++ 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/modules/api/php/views/project.class.inc b/modules/api/php/views/project.class.inc index 387b53cb7db..27aaa3e4290 100644 --- a/modules/api/php/views/project.class.inc +++ b/modules/api/php/views/project.class.inc @@ -29,7 +29,7 @@ class Project * * @var \Project */ - protected $_project; + private $_project; /** * Constructor @@ -55,7 +55,7 @@ class Project 'Meta' => $meta, 'Candidates' => $this->_project->getCandidateIds(), 'Instruments' => array_keys(\Utility::getAllInstruments()), - 'Visits' => $this->getVisits(), + 'Visits' => $this->_getVisits(), ]; } @@ -87,7 +87,7 @@ class Project return [ 'Meta' => $meta, - 'Visits' => $this->getVisits(), + 'Visits' => $this->_getVisits(), ]; } @@ -96,7 +96,7 @@ class Project * * @return array */ - protected function getVisits(): array + private function _getVisits(): array { // TODO :: This should be replaced by $this->_project->getVisitLabels(); return array_keys( diff --git a/modules/api/php/views/project_0_0_4_dev.class.inc b/modules/api/php/views/project_0_0_4_dev.class.inc index b8df8bd15f5..28583688c7c 100644 --- a/modules/api/php/views/project_0_0_4_dev.class.inc +++ b/modules/api/php/views/project_0_0_4_dev.class.inc @@ -20,8 +20,25 @@ namespace LORIS\api\Views; * @link https://www.github.com/aces/Loris/ */ -class Project_0_0_4_Dev extends Project +class Project_0_0_4_Dev { + /** + * The project to format + * + * @var \Project + */ + protected $_project; + + /** + * Constructor + * + * @param \Project $project The project to format + */ + public function __construct(\Project $project) + { + $this->_project = $project; + } + /** * Produce an array representation of this project. * @@ -29,18 +46,18 @@ class Project_0_0_4_Dev extends Project */ public function toArray(): array { - $meta = ['Project' => $this->_project->getName()]; + $name = $this->_project->getName(); return [ - 'Meta' => $meta, - 'Subprojects' => array_values( - \Utility::getSubprojectList( - $this->_project->getId() - ) - ), - 'Candidates' => $this->_project->getCandidateIds(), - 'Instruments' => array_keys(\Utility::getAllInstruments()), - 'Visits' => $this->getVisits(), + 'Id' => $this->_project->getId(), + 'Name' => $this->_project->getName(), + 'Alias' => $this->_project->getAlias(), + 'RecruitementTarget' => $this->_project->getRecruitmentTarget(), + 'Links' => [ + 'Candidates' => "/projects/$name/candidates", + 'Instruments' => "/projects/$name/instruments", + 'Subprojects' => "/projects/$name/subprojects" + ] ]; } } diff --git a/modules/api/static/schema-v0.0.4-dev.yml b/modules/api/static/schema-v0.0.4-dev.yml index 2a35ed78050..54887f3e70e 100644 --- a/modules/api/static/schema-v0.0.4-dev.yml +++ b/modules/api/static/schema-v0.0.4-dev.yml @@ -14,6 +14,27 @@ servers: security: - ApiKeyAuth: [] paths: + '/projects/{project}': + get: + tags: + - Projects + summary: Get that project descriptor + parameters: + - name: project + in: path + description: The project name + required: true + style: simple + explode: false + schema: + type: string + responses: + '200': + description: An object containing the project descriptor + content: + application/json: + schema: + $ref: '#/components/schemas/Project' '/projects/{project}/subprojects': get: tags: @@ -66,6 +87,26 @@ paths: $ref: '#/components/schemas/ProjectSubproject' components: schemas: + Project: + type: object + properties: + Id: + type: string + Name: + type: string + Alias: + type: string + RecruitementTarget: + type: integer + Links: + type: object + properties: + Candidates: + type: string + Instruments: + type: string + Subprojects: + type: string ProjectSubprojects: type: object properties: