diff --git a/modules/api/docs/LorisRESTAPI_v0.0.4-dev.md b/modules/api/docs/LorisRESTAPI_v0.0.4-dev.md index 42e2671edf8..1a248b722a8 100644 --- a/modules/api/docs/LorisRESTAPI_v0.0.4-dev.md +++ b/modules/api/docs/LorisRESTAPI_v0.0.4-dev.md @@ -462,9 +462,10 @@ Loris, or Approval has not occurred) ### 3.3 Candidate Instruments ``` GET /candidates/$CandID/$VisitLabel/instruments +POST /candidates/$CandID/$VisitLabel/instruments ``` -Will return a JSON object of the form: +GET will return a JSON object of the form: ```js { @@ -480,7 +481,10 @@ Where the instruments array represents the instruments that were administered fo candidate at that visit. InstrumentNames are the short names and the forms for them SHOULD all be retrievable through the `project` portion of the API. -PUT / PATCH / POST are not currently supported for candidate instruments. +POST accepts data of the same format. Any instruments in the Instrument key not currently in the visit test battery will be added to +the battery. + +PUT / PATCH are not currently supported for candidate instruments. #### 3.3.1 The Candidate Instrument Data diff --git a/modules/api/php/endpoints/candidate/visit/instruments.class.inc b/modules/api/php/endpoints/candidate/visit/instruments.class.inc index ca537b379cd..4324666def2 100644 --- a/modules/api/php/endpoints/candidate/visit/instruments.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instruments.class.inc @@ -57,7 +57,7 @@ class Instruments extends Endpoint implements \LORIS\Middleware\ETagCalculator */ protected function allowedMethods() : array { - return ['GET']; + return ['GET', 'POST']; } /** @@ -85,12 +85,18 @@ class Instruments extends Endpoint implements \LORIS\Middleware\ETagCalculator { $pathparts = $request->getAttribute('pathparts'); $loris = $request->getAttribute("loris"); + $version = $request->getAttribute("LORIS-API-Version"); if (count($pathparts) === 0) { switch ($request->getMethod()) { case 'GET': return $this->_handleGET($request); - + case 'POST': + return $this->_handlePOST($request); case 'OPTIONS': + if ($version == 'v0.0.3') { + return (new \LORIS\Http\Response()) + ->withHeader('Allow', ['GET']); + } return (new \LORIS\Http\Response()) ->withHeader('Allow', $this->allowedMethods()); @@ -166,6 +172,63 @@ class Instruments extends Endpoint implements \LORIS\Middleware\ETagCalculator return $this->_cache; } + /** + * Handle an incoming post request. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface The outgoing PSR7 response + */ + private function _handlePOST(ServerRequestInterface $request): ResponseInterface + { + $version = $request->getAttribute("LORIS-API-Version"); + $user = $request->getAttribute("user"); + $loris = $request->getAttribute("loris"); + + if ($version == 'v0.0.3') { + return new \LORIS\Http\Response\JSON\NotFound(); + } + + $requestdata = json_decode($request->getBody()->__toString(), true); + if (empty($requestdata)) { + return new \LORIS\Http\Response\JSON\BadRequest(); + } + foreach ($requestdata['Instruments'] as $instrument) { + try { + $instrument = \NDB_BVL_Instrument::factory( + $loris, + $instrument, + '', + '', + true + ); + if (!$instrument->_hasAccess($user)) { + return new \LORIS\Http\Response\JSON\Forbidden(); + } + } catch (\NotFound $e) { + return new \LORIS\Http\Response\JSON\NotFound( + "Instrument does not exist" + ); + } catch (\Exception $e) { + return new \LORIS\Http\Response\JSON\InternalServerError(); + } + } + $battery = new \NDB_BVL_Battery(); + $battery->selectBattery($this->_visit->getSessionID()); + $existingBattery = $battery->getBattery(); + foreach ($requestdata['Instruments'] as $instrument) { + if (in_array($instrument, $existingBattery, true)) { + continue; + } + $battery->addInstrument($loris, $instrument); + } + + $view = (new \LORIS\api\Views\Visit\Instruments( + $this->_visit + ))->toArray(); + return new \LORIS\Http\Response\JsonResponse($view); + } + /** * Implements the ETagCalculator interface * diff --git a/modules/api/static/schema-v0.0.4-dev.yml b/modules/api/static/schema-v0.0.4-dev.yml index d430063149b..2c60de39ea0 100644 --- a/modules/api/static/schema-v0.0.4-dev.yml +++ b/modules/api/static/schema-v0.0.4-dev.yml @@ -404,6 +404,39 @@ paths: application/json: schema: $ref: '#/components/schemas/inline_response_200_4' + post: + tags: + - Instruments + summary: Add instruments the the visit test battery + parameters: + - name: id + in: path + description: The candidate identifier. Either a PSCID or CandID. + required: true + style: simple + explode: false + schema: + type: string + - name: visit + in: path + description: The visit label + required: true + style: simple + explode: false + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/inline_response_200_4' + responses: + '200': + description: The battery was successfully updated. Returns the new battery + content: + application/json: + schema: + $ref: '#/components/schemas/inline_response_200_4' '/candidates/{id}/{visit}/instruments/{instrument}': get: tags: diff --git a/raisinbread/test/api/LorisApiInstrumentsTest.php b/raisinbread/test/api/LorisApiInstrumentsTest.php index 1a3dd80884c..01dc5eefcf2 100644 --- a/raisinbread/test/api/LorisApiInstrumentsTest.php +++ b/raisinbread/test/api/LorisApiInstrumentsTest.php @@ -67,6 +67,95 @@ public function testGetCandidatesCandidVisitInstruments(): void ); } + /** + * Tests the HTTP POST request for the + * endpoint /candidates/{candid}/{visit}/instruments + * + * @return void + */ + public function testPostCandidatesCandidVisitInstruments(): void + { + // Remove all instruments from this CandID. + $SessionID = $this->DB->pselectOne( + "SELECT ID FROM session WHERE Visit_label=:VL AND CandID=:Candidate", + [ + 'VL' => $this->visitTest, + 'Candidate' => $this->candidTest + ] + ); + $this->DB->delete("flag", ['SessionID' => $SessionID]); + + // Insert one + $json_data = [ + 'Meta' => [ + 'CandID' => $this->candidTest, + 'Visit' => $this->visitTest, + ], + "Instruments" => [ 'bmi' ], + ]; + $response = $this->client->request( + 'POST', + "candidates/$this->candidTest/$this->visitTest/instruments", + [ + 'http_errors' => false, + 'headers' => $this->headers, + 'json' => $json_data, + ] + ); + $this->assertEquals(200, $response->getStatusCode()); + // Verify the endpoint has a body + $body = $response->getBody(); + $this->assertNotEmpty($body); + + $instrArray = json_decode( + (string) utf8_encode( + $response->getBody()->getContents() + ), + true + ); + + $this->assertSame(gettype($instrArray), 'array'); + + $this->assertArrayHasKey( + 'CandID', + $instrArray['Meta'] + ); + $this->assertArrayHasKey( + 'Visit', + $instrArray['Meta'] + ); + $this->assertArrayHasKey( + '0', + $instrArray['Instruments'] + ); + $this->assertEquals($instrArray['Instruments'], ['bmi']); + + // Insert another and make sure they are both there now. + $json_data = [ + 'Meta' => [ + 'CandID' => $this->candidTest, + 'Visit' => $this->visitTest, + ], + "Instruments" => [ 'testtest' ], + ]; + $response = $this->client->request( + 'POST', + "candidates/$this->candidTest/$this->visitTest/instruments", + [ + 'http_errors' => false, + 'headers' => $this->headers, + 'json' => $json_data, + ] + ); + $instrArray = json_decode( + (string) utf8_encode( + $response->getBody()->getContents() + ), + true + ); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals($instrArray['Instruments'], ['bmi', 'testtest']); + } /** * Tests the HTTP GET request for the * endpoint /candidates/{candid}/{visit}/instruments{instruments}