diff --git a/modules/api/docs/LorisRESTAPI_v0.0.4-dev.md b/modules/api/docs/LorisRESTAPI_v0.0.4-dev.md index a03be523543..e511e60d928 100644 --- a/modules/api/docs/LorisRESTAPI_v0.0.4-dev.md +++ b/modules/api/docs/LorisRESTAPI_v0.0.4-dev.md @@ -416,10 +416,10 @@ The JSON object is of the form: } ``` -A PUT of the same format but with only the Meta fields will create the VisitLabel +A PUT request of the same format but with only the Meta fields will create the VisitLabel for this candidate, in an unstarted stage if the Visit label provided is valid. -PATCH is not supported for Visit Labels. +A PATCH request with the Meta fields and a date NextStageDate will start the next stage, if the stage is 'Not Started' and the CandID and Visit label provided are valid. It will return a 404 Not Found if the visit label does not exist for this candidate (as well as anything under the /candidates/$CandID/$VisitLabel hierarchy) diff --git a/modules/api/php/endpoints/candidate/visit/visit.class.inc b/modules/api/php/endpoints/candidate/visit/visit.class.inc index 526a18279e9..47cdaad924b 100644 --- a/modules/api/php/endpoints/candidate/visit/visit.class.inc +++ b/modules/api/php/endpoints/candidate/visit/visit.class.inc @@ -42,6 +42,20 @@ class Visit extends Endpoint implements \LORIS\Middleware\ETagCalculator */ private $_visit; + /** + * The visit centerID + * + * @var ?\CenterID + */ + private $_centerID; + + /** + * The visit Project + * + * @var ?\Project + */ + private $_project; + /** * Contructor * @@ -63,6 +77,7 @@ class Visit extends Endpoint implements \LORIS\Middleware\ETagCalculator { return [ 'GET', + 'PATCH', 'PUT', ]; } @@ -95,7 +110,6 @@ class Visit extends Endpoint implements \LORIS\Middleware\ETagCalculator if (!$this->_visit->isAccessibleBy($user)) { return new \LORIS\Http\Response\JSON\Forbidden('Permission denied'); } - } $pathparts = $request->getAttribute('pathparts'); @@ -104,6 +118,9 @@ class Visit extends Endpoint implements \LORIS\Middleware\ETagCalculator case 'GET': return $this->_handleGET(); + case 'PATCH': + return $this->_handlePATCH($request); + case 'PUT': return $this->_handlePUT($request); @@ -172,23 +189,22 @@ class Visit extends Endpoint implements \LORIS\Middleware\ETagCalculator } /** - * Handles a PUT request that creates or replace a candidate visit - * - * TODO: There is no way to validate the the visit_label in the url - * fits the json data of the request because it is removed from the - * pathparts in the calling class. The correct way to do it would be - * to pass a "light" timepoint class that contains the visit_label but - * no sessionid in the constructor. + * Validate a PUT request * * @param ServerRequestInterface $request The incoming PSR7 request * - * @return ResponseInterface The outgoing PSR7 response + * @return ?ResponseInterface The outgoing PSR7 response */ - private function _handlePUT(ServerRequestInterface $request): ResponseInterface - { - $user = $request->getAttribute('user'); - $data = json_decode((string) $request->getBody(), true); - $visitinfo = $data ?? []; + private function _validatePUT( + ServerRequestInterface $request + ): ?ResponseInterface { + $user = $request->getAttribute('user'); + $data = json_decode((string) $request->getBody(), true); + $meta = $data ?? []; + + if ($this->_visit === null) { + return new \LORIS\Http\Response\JSON\NotFound('Visit not found'); + } $requiredfields = [ 'CandID', @@ -197,7 +213,7 @@ class Visit extends Endpoint implements \LORIS\Middleware\ETagCalculator 'Battery', 'Project', ]; - $diff = array_diff($requiredfields, array_keys($visitinfo)); + $diff = array_diff($requiredfields, array_keys($meta)); if (!empty($diff)) { return new \LORIS\Http\Response\JSON\BadRequest( @@ -206,16 +222,16 @@ class Visit extends Endpoint implements \LORIS\Middleware\ETagCalculator } try { - $project = \NDB_Factory::singleton()->project($visitinfo['Project']); + $this->_project = \NDB_Factory::singleton()->project($meta['Project']); } catch (\NotFound $e) { return new \LORIS\Http\Response\JSON\BadRequest( $e->getMessage() ); } - if ($visitinfo['CandID'] !== $this->_candidate->getCandID()) { + if ($meta['CandID'] !== $this->_candidate->getCandID()) { return new \LORIS\Http\Response\JSON\BadRequest( - 'CandID do not match this candidate' + 'The CandID meta field does not match this candidate' ); } @@ -225,20 +241,132 @@ class Visit extends Endpoint implements \LORIS\Middleware\ETagCalculator ); } - $centerid = array_search($visitinfo['Site'], \Utility::getSiteList()); + if ($meta['Visit'] !== $this->_visit->getVisitLabel()) { + return new \LORIS\Http\Response\JSON\BadRequest( + 'The Visit meta field does not match this visit' + ); + } + + $centerID = array_search($meta['Site'], \Utility::getSiteList()); - if ($centerid === false - || !in_array(new \CenterID("$centerid"), $user->getCenterIDs()) + if ($centerID === false || !in_array( + new \CenterID(strval($this->_centerID)), + $user->getCenterIDs() + ) ) { return new \LORIS\Http\Response\JSON\Forbidden( "You can't create or modify candidates visit for the site " . - $centerid + $this->_centerID ); } - // \Utility::getSiteList key was a string. Now that the - // validation is done, convert to an object. - $centerid = new \CenterID("$centerid"); + // Now that the validation is done, convert to an object. + $this->_centerID = new \CenterID(strval($centerID)); + } + + /** + * Handles a PATCH request. + * For now, only support a NextStageDate argument to start next stage. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface The outgoing PSR7 response + */ + private function _handlePATCH(ServerRequestInterface $request): ResponseInterface + { + $data = json_decode((string) $request->getBody(), true); + $nextStageDate = $data['NextStageDate'] ?? null; + + $response = $this->_validatePUT($request); + if ($response !== null) { + return $response; + } + + if (!$nextStageDate) { + return new \LORIS\Http\Response\JSON\BadRequest( + 'There is no stage date specified' + ); + } + + $date_format = 'YYYY-MM-DD'; + $date = \DateTime::createFromFormat($date_format, $nextStageDate); + if (!($date && $date->format($date_format) === $nextStageDate)) { + return new \LORIS\Http\Response\JSON\BadRequest( + 'The date specified is invalid. Date must be of the form YYYY-MM-DD' + ); + } + + if ($nextStageDate > date($date_format)) { + return new \LORIS\Http\Response\JSON\BadRequest( + 'The date specified must be not later than today' + ); + } + + if ($this->_visit->getCurrentStage() !== 'Not Started') { + return new \LORIS\Http\Response\JSON\Conflict( + 'This visit is already started' + ); + } + + $newStage = $this->_visit->getNextStage(); + if (!$newStage) { + return new \LORIS\Http\Response\JSON\Conflict( + "Next stage is null" + ); + } + + // start that stage + $this->_visit->startStage($newStage); + + // set the date for the new stage + $this->_visit->setData(["Date_".$newStage => $nextStageDate]); + + // create a new battery object && new battery + $candidate = \Candidate::singleton($this->_visit->getCandID()); + + // First visit ? + $firstVisit = false; + try { + if ($candidate->getFirstVisit() == $this->_visit->getVisitLabel()) { + $firstVisit = true; + } + } catch (\LorisException $e) { + } + + $battery = new \NDB_BVL_Battery; + // select a specific time point (sessionID) for the battery + $battery->selectBattery($this->_visit->getSessionID()); + + // add instruments to the time point + $battery->createBattery( + $request->getAttribute("loris"), + $this->_visit->getSubprojectID(), + $newStage, + $this->_visit->getVisitLabel(), + $this->_visit->getCenterID(), + $firstVisit + ); + + return new \LORIS\Http\Response\JSON\OK(); + } + + /** + * Handles a PUT request that creates or replace a candidate visit + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface The outgoing PSR7 response + */ + private function _handlePUT(ServerRequestInterface $request): ResponseInterface + { + $user = $request->getAttribute('user'); + $data = json_decode((string) $request->getBody(), true); + $visitinfo = $data ?? []; + + $response = $this->_validatePUT($request); + if ($response !== null) { + return $response; + } $subprojectid = array_search( $visitinfo['Battery'], @@ -255,6 +383,7 @@ class Visit extends Endpoint implements \LORIS\Middleware\ETagCalculator $timepoint = \NDB_Factory::singleton()->timepoint( new \SessionID(strval($sessionid)) ); + if ($timepoint->getCurrentStage() !== 'Not Started') { return new \LORIS\Http\Response\JSON\Conflict( 'This visit is already started' @@ -266,7 +395,7 @@ class Visit extends Endpoint implements \LORIS\Middleware\ETagCalculator $timepoint->setData( [ - 'CenterID' => $centerid, + 'CenterID' => $this->_centerID, 'Visit_label' => $visitinfo['Visit'], 'SubprojectID' => $subprojectid, 'Active' => 'Y', @@ -275,7 +404,7 @@ class Visit extends Endpoint implements \LORIS\Middleware\ETagCalculator 'UserID' => $username, 'Date_registered' => $today, 'Testdate' => $today, - 'ProjectID' => $project->getId(), + 'ProjectID' => $this->_project->getId(), ] ); @@ -301,8 +430,8 @@ class Visit extends Endpoint implements \LORIS\Middleware\ETagCalculator $this->_candidate, $subprojectid, $visitinfo['Visit'], - \Site::singleton($centerid), - $project + \Site::singleton($this->_centerID), + $this->_project ); $link = '/' . $request->getUri()->getPath();