diff --git a/controller/RestController.php b/controller/RestController.php index c62eb4216..f9366cfcf 100644 --- a/controller/RestController.php +++ b/controller/RestController.php @@ -1063,4 +1063,59 @@ public function related($request) $ret = $this->transformPropertyResults($request->getUri(), $request->getLang(), $related, "related", "skos:related"); return $this->returnJson($ret); } + + /** + * Used for querying new concepts in the vocabulary + * @param Request $request + * @return object json-ld wrapped list of changed concepts + */ + public function newConcepts($request) + { + $offset = ($request->getQueryParam('offset') && is_numeric($request->getQueryParam('offset')) && $request->getQueryParam('offset') >= 0) ? $request->getQueryParam('offset') : 0; + $limit = ($request->getQueryParam('limit') && is_numeric($request->getQueryParam('limit')) && $request->getQueryParam('limit') >= 0) ? $request->getQueryParam('limit') : 200; + + return $this->changedConcepts($request, 'dc:created', $offset, $limit); + } + + /** + * Used for querying modified concepts in the vocabulary + * @param Request $request + * @return object json-ld wrapped list of changed concepts + */ + public function modifiedConcepts($request) + { + $offset = ($request->getQueryParam('offset') && is_numeric($request->getQueryParam('offset')) && $request->getQueryParam('offset') >= 0) ? $request->getQueryParam('offset') : 0; + $limit = ($request->getQueryParam('limit') && is_numeric($request->getQueryParam('limit')) && $request->getQueryParam('limit') >= 0) ? $request->getQueryParam('limit') : 200; + + return $this->changedConcepts($request, 'dc:modified', $offset, $limit); + } + + /** + * Used for querying changed concepts in the vocabulary + * @param Request $request + * @param int $offset starting index offset + * @param int $limit maximum number of concepts to return + * @return object json-ld wrapped list of changed concepts + */ + private function changedConcepts($request, $prop, $offset, $limit) + { + $changeList = $request->getVocab()->getChangeList($prop, $request->getLang(), $offset, $limit); + + $simpleChangeList = array(); + foreach($changeList as $conceptInfo) { + if (array_key_exists('date', $conceptInfo)) { + $simpleChangeList[] = array( 'uri' => $conceptInfo['uri'], + 'prefLabel' => $conceptInfo['prefLabel'], + 'date' => $conceptInfo['date']->format("Y-m-d\TH:i:sO") ); + } + } + return $this->returnJson(array_merge_recursive($this->context, + array('@context' => array( '@language' => $request->getLang(), + 'prefLabel' => 'skos:prefLabel', + 'xsd' => 'http://www.w3.org/2001/XMLSchema#', + 'date' => array( '@id' => 'http://purl.org/dc/terms/date', '@type' => 'http://www.w3.org/2001/XMLSchema#dateTime') ) + ), + array('changeList' => $simpleChangeList))); + + } } diff --git a/controller/WebController.php b/controller/WebController.php index ee547c8f4..37ed988b2 100644 --- a/controller/WebController.php +++ b/controller/WebController.php @@ -559,22 +559,53 @@ public function invokeGenericErrorPage($request, $message = null) */ public function invokeChangeList($request, $prop='dc:created') { - // set language parameters for gettext - $this->setLanguageProperties($request->getLang()); - $vocab = $request->getVocab(); $offset = ($request->getQueryParam('offset') && is_numeric($request->getQueryParam('offset')) && $request->getQueryParam('offset') >= 0) ? $request->getQueryParam('offset') : 0; - $changeList = $vocab->getChangeList($prop, $request->getContentLang(), $request->getLang(), $offset); + $limit = ($request->getQueryParam('limit') && is_numeric($request->getQueryParam('limit')) && $request->getQueryParam('limit') >= 0) ? $request->getQueryParam('limit') : 200; + + $changeList = $this->getChangeList($request, $prop, $offset, $limit); + $bydate = $this->formatChangeList($changeList, $request->getLang()); + // load template $template = $this->twig->loadTemplate('changes.twig'); // render template echo $template->render( array( - 'vocab' => $vocab, + 'vocab' => $request->getVocab(), 'languages' => $this->languages, 'request' => $request, - 'changeList' => $changeList) + 'changeList' => $bydate) ); } + /** + * Gets the list of newest concepts for a vocabulary according to timestamp indicated by a property + * @param Request $request + * @param string $prop the name of the property eg. 'dc:modified'. + * @param int $offset starting index offset + * @param int $limit maximum number of concepts to return + * @return Array list of concepts + */ + public function getChangeList($request, $prop, $offset=0, $limit=200) + { + // set language parameters for gettext + $this->setLanguageProperties($request->getLang()); + + return $request->getVocab()->getChangeList($prop, $request->getContentLang(), $offset, $limit); + } + + /** + * Formats the list of concepts as labels arranged by modification month + * @param Array $changeList + * @param string $lang the language for displaying dates in the change list + */ + public function formatChangeList($changeList, $lang) + { + $formatByDate = array(); + foreach($changeList as $concept) { + $concept['datestring'] = Punic\Calendar::formatDate($concept['date'], 'medium', $lang); + $formatByDate[Punic\Calendar::getMonthName($concept['date'], 'wide', $lang, true) . Punic\Calendar::format($concept['date'], ' y', $lang) ][strtolower($concept['prefLabel'])] = $concept; + } + return $formatByDate; + } } diff --git a/model/Vocabulary.php b/model/Vocabulary.php index a671f2598..abd25df95 100644 --- a/model/Vocabulary.php +++ b/model/Vocabulary.php @@ -615,19 +615,15 @@ public function verifyVocabularyLanguage($lang) /** * Returns a list of recently changed or entirely new concepts. + * @param string $prop the property uri pointing to timestamps, eg. 'dc:modified' * @param string $clang content language for the labels - * @param string $lang UI language for the dates + * @param int $offset starting index offset + * @param int $limit maximum number of concepts to return * @return Array */ - public function getChangeList($prop, $clang, $lang, $offset) + public function getChangeList($prop, $clang, $offset, $limit) { - $changelist = $this->getSparql()->queryChangeList($clang, $offset, $prop); - $bydate = array(); - foreach($changelist as $concept) { - $concept['datestring'] = Punic\Calendar::formatDate($concept['date'], 'medium', $lang); - $bydate[Punic\Calendar::getMonthName($concept['date'], 'wide', $lang, true) . Punic\Calendar::format($concept['date'], ' y', $lang) ][strtolower($concept['prefLabel'])] = $concept; - } - return $bydate; + return $this->getSparql()->queryChangeList($prop, $clang, $offset, $limit); } public function getTitle($lang=null) { diff --git a/model/sparql/GenericSparql.php b/model/sparql/GenericSparql.php index d1e3fd63c..f47dceeae 100644 --- a/model/sparql/GenericSparql.php +++ b/model/sparql/GenericSparql.php @@ -2213,11 +2213,13 @@ public function listConceptGroupContents($groupClass, $group, $lang,$showDepreca /** * Generates the sparql query for queryChangeList. - * @param string $lang language of labels to return. + * @param string $prop the property uri pointing to timestamps, eg. 'dc:modified' + * @param string $lang language of labels to return * @param int $offset offset of results to retrieve; 0 for beginning of list + * @param int $limit maximum number of results to return * @return string sparql query */ - private function generateChangeListQuery($lang, $offset, $prop) { + private function generateChangeListQuery($prop, $lang, $offset, $limit=200) { $fcl = $this->generateFromClause(); $offset = ($offset) ? 'OFFSET ' . $offset : ''; @@ -2230,8 +2232,9 @@ private function generateChangeListQuery($lang, $offset, $prop) { FILTER (langMatches(lang(?label), '$lang')) } ORDER BY DESC(YEAR(?date)) DESC(MONTH(?date)) LCASE(?label) -LIMIT 200 $offset +LIMIT $limit $offset EOQ; + return $query; } @@ -2249,7 +2252,12 @@ private function transformChangeListResults($result) { } if (isset($row->date)) { - $concept['date'] = $row->date->getValue(); + try { + $concept['date'] = $row->date->getValue(); + } catch (Exception $e) { + //don't record concepts with malformed dates e.g. 1986-21-00 + continue; + } } $ret[] = $concept; @@ -2259,12 +2267,15 @@ private function transformChangeListResults($result) { /** * return a list of recently changed or entirely new concepts + * @param string $prop the property uri pointing to timestamps, eg. 'dc:modified' * @param string $lang language of labels to return * @param int $offset offset of results to retrieve; 0 for beginning of list + * @param int $limit maximum number of results to return * @return array Result array */ - public function queryChangeList($lang, $offset, $prop) { - $query = $this->generateChangeListQuery($lang, $offset, $prop); + public function queryChangeList($prop, $lang, $offset, $limit) { + $query = $this->generateChangeListQuery($prop, $lang, $offset, $limit); + $result = $this->query($query); return $this->transformChangeListResults($result); } diff --git a/rest.php b/rest.php index 5d276f105..ddd884fcd 100644 --- a/rest.php +++ b/rest.php @@ -97,6 +97,10 @@ $controller->groups($request); } elseif ($parts[2] == 'groupMembers') { $controller->groupMembers($request); + } elseif ($parts[2] == 'new') { + $controller->newConcepts($request); + } elseif ($parts[2] == 'modified') { + $controller->modifiedConcepts($request); } else { header("HTTP/1.0 404 Not Found"); echo ("404 Not Found"); diff --git a/swagger.json b/swagger.json index 37e43070c..7bfbe25ee 100644 --- a/swagger.json +++ b/swagger.json @@ -1121,6 +1121,110 @@ ] } }, + "/{vocid}/new": { + "get": { + "summary": "New concepts in the vocabulary", + "parameters": [ + { + "name": "vocid", + "in": "path", + "description": "a Skosmos vocabulary identifier e.g. \"stw\" or \"yso\"", + "required": true, + "type": "string" + }, + { + "name": "lang", + "in": "query", + "description": "label language, e.g. \"en\" or \"fi\"", + "required": false, + "type": "string" + }, + { + "name": "offset", + "in": "query", + "description": "offset of the starting index", + "required": false, + "type": "number" + }, + { + "name": "limit", + "in": "query", + "description": "maximum number of concepts to return", + "required": false, + "type": "number" + } + ], + "produces": [ + "application/ld+json" + ], + "responses": { + "200": { + "description": "list of most recently created concepts of the vocabulary", + "schema": { + "$ref": "#/definitions/changedConceptsResult" + } + }, + "404": { + "description": "no vocabulary could be found with the requested id" + } + }, + "tags": [ + "Vocabulary-specific methods" + ] + } + }, + "/{vocid}/modified": { + "get": { + "summary": "Modified concepts in the vocabulary", + "parameters": [ + { + "name": "vocid", + "in": "path", + "description": "a Skosmos vocabulary identifier e.g. \"stw\" or \"yso\"", + "required": true, + "type": "string" + }, + { + "name": "lang", + "in": "query", + "description": "label language, e.g. \"en\" or \"fi\"", + "required": false, + "type": "string" + }, + { + "name": "offset", + "in": "query", + "description": "offset of the starting index", + "required": false, + "type": "number" + }, + { + "name": "limit", + "in": "query", + "description": "maximum number of concepts to return", + "required": false, + "type": "number" + } + ], + "produces": [ + "application/ld+json" + ], + "responses": { + "200": { + "description": "list of most recently created concepts of the vocabulary", + "schema": { + "$ref": "#/definitions/changedConceptsResult" + } + }, + "404": { + "description": "no vocabulary could be found with the requested id" + } + }, + "tags": [ + "Vocabulary-specific methods" + ] + } + }, "/{vocid}/groupMembers": { "get": { "summary": "Members of the requested concept group", @@ -2281,6 +2385,44 @@ "vocabName", "typeLabel" ] + }, + "changedConceptsResult": { + "type": "object", + "properties": { + "changeList": { + "type": "array", + "description": "List of changed concepts", + "items": { + "$ref": "#/definitions/changedConcept" + } + } + }, + "required": [ + "changeList" + ] + }, + "changedConcept": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "URI of the concept" + }, + "prefLabel": { + "type": "string", + "description": "Preferred label of the concept" + }, + "date": { + "type": "string", + "format": "date-time", + "description": "Time stamp" + } + }, + "required": [ + "uri", + "prefLabel", + "date" + ] } } } \ No newline at end of file diff --git a/tests/GenericSparqlTest.php b/tests/GenericSparqlTest.php index 12f752c63..4202936e0 100644 --- a/tests/GenericSparqlTest.php +++ b/tests/GenericSparqlTest.php @@ -1098,7 +1098,7 @@ public function testQueryChangeList() $voc = $this->model->getVocabulary('changes'); $graph = $voc->getGraph(); $sparql = new GenericSparql('http://localhost:13030/skosmos-test/sparql', $graph, $this->model); - $actual = $sparql->queryChangeList('en', 0, 'dc11:created'); + $actual = $sparql->queryChangeList('dc:created', 'en', 0, 10); $order = array(); foreach($actual as $concept) { array_push($order, $concept['prefLabel']); @@ -1107,6 +1107,23 @@ public function testQueryChangeList() $this->assertEquals(array('Fourth date', 'Hurr Durr', 'Second date', 'A date'), $order); } + /** + * @covers GenericSparql::queryChangeList + * @covers GenericSparql::generateChangeListQuery + * @covers GenericSparql::transFormChangeListResults + */ + public function testMalformedDates() { + $voc = $this->model->getVocabulary('test'); + $graph = $voc->getGraph(); + $sparql = new GenericSparql('http://localhost:13030/skosmos-test/sparql', $graph, $this->model); + $result = $sparql->queryChangeList('dc:modified', 'en', 0, 10); + $uris = array(); + foreach($result as $concept) { + $uris[] = $concept['uri']; + } + $this->assertNotContains('http://www.skosmos.skos/test/ta114', $uris); + } + /** * @covers GenericSparql::formatTypes * @covers GenericSparql::queryConcepts diff --git a/tests/RestControllerTest.php b/tests/RestControllerTest.php index e5180d00c..fbcb2ae9f 100644 --- a/tests/RestControllerTest.php +++ b/tests/RestControllerTest.php @@ -340,4 +340,84 @@ public function testLabelNoConcept() { $this->assertEquals($expected, $out); } + /** + * @covers RestController::newConcepts + */ + public function testNewConcepts() { + $request = new Request($this->model); + $request->setVocab('changes'); + $request->setLang('en'); + $request->setContentLang('en'); + $request->setQueryParam('offset', '0'); + + $this->controller->newConcepts($request); + $changeList = $this->getActualOutput(); + + $expected = <<assertJsonStringEqualsJsonString($changeList, $expected); + + } + + /** + * @covers RestController::modifiedConcepts + */ + public function testModifiedConcepts() { + $request = new Request($this->model); + $request->setVocab('test'); + $request->setLang('en'); + $request->setContentLang('en'); + $request->setQueryParam('offset', '0'); + + $this->controller->modifiedConcepts($request); + $changeList = $this->getActualOutput(); + + $expected = <<assertJsonStringEqualsJsonString($changeList, $expected); + + } } diff --git a/tests/VocabularyTest.php b/tests/VocabularyTest.php index 6a813704b..8cfe693c1 100644 --- a/tests/VocabularyTest.php +++ b/tests/VocabularyTest.php @@ -433,10 +433,9 @@ public function testGetTopConcepts() { */ public function testGetChangeList() { $vocab = $this->model->getVocabulary('changes'); - $months = $vocab->getChangeList('dc11:created','en', 'en', 0); - $expected = array ('hurr durr' => array ('uri' => 'http://www.skosmos.skos/changes/d3', 'prefLabel' => 'Hurr Durr', 'date' => DateTime::__set_state(array('date' => '2010-02-12 10:26:39.000000', 'timezone_type' => 3, 'timezone' => 'UTC')), 'datestring' => 'Feb 12, 2010'), 'second date' => array ('uri' => 'http://www.skosmos.skos/changes/d2', 'prefLabel' => 'Second date', 'date' => DateTime::__set_state(array('date' => '2010-02-12 15:26:39.000000', 'timezone_type' => 3, 'timezone' => 'UTC')), 'datestring' => 'Feb 12, 2010')); - $this->assertEquals(array('December 2011', 'February 2010', 'January 2000'), array_keys($months)); - $this->assertEquals($expected, $months['February 2010']); + $changeList = $vocab->getChangeList('dc:created','en', 0, 5); + $expected = array ('uri' => 'http://www.skosmos.skos/changes/d3', 'prefLabel' => 'Hurr Durr', 'date' => DateTime::__set_state(array('date' => '2010-02-12 10:26:39.000000', 'timezone_type' => 3, 'timezone' => 'UTC'))); + $this->assertEquals($expected, $changeList[1]); } /** diff --git a/tests/WebControllerTest.php b/tests/WebControllerTest.php index f9d401b41..1a9bfcbaf 100644 --- a/tests/WebControllerTest.php +++ b/tests/WebControllerTest.php @@ -4,6 +4,14 @@ class WebControllerTest extends TestCase { + private $webController; + private $model; + + protected function setUp() { + $globalConfig = new GlobalConfig('/../tests/testconfig.ttl'); + $this->model = Mockery::mock(new Model($globalConfig)); + $this->webController = new WebController($this->model); + } /** * Data for testGetGitModifiedDateCacheEnabled and for testGetConfigModifiedDate. We are able to use the @@ -199,4 +207,23 @@ public function testGetModifiedDate($concept, $git, $config, $modifiedDate) $returnedValue = $controller->getModifiedDate($modifiable); $this->assertEquals($modifiedDate, $returnedValue); } + + /** + * @covers WebController::getChangeList + * @covers WebController::formatChangeList + */ + public function testFormatChangeList() { + $request = new Request($this->model); + $request->setVocab('changes'); + $request->setLang('en'); + $request->setContentLang('en'); + $request->setQueryParam('offset', '0'); + + $changeList = $this->webController->getChangeList($request, 'dc:created'); + $months =$this->webController->formatChangeList($changeList, 'en'); + + $expected = array ('hurr durr' => array ('uri' => 'http://www.skosmos.skos/changes/d3', 'prefLabel' => 'Hurr Durr', 'date' => DateTime::__set_state(array('date' => '2010-02-12 10:26:39.000000', 'timezone_type' => 3, 'timezone' => 'UTC')), 'datestring' => 'Feb 12, 2010'), 'second date' => array ('uri' => 'http://www.skosmos.skos/changes/d2', 'prefLabel' => 'Second date', 'date' => DateTime::__set_state(array('date' => '2010-02-12 15:26:39.000000', 'timezone_type' => 3, 'timezone' => 'UTC')), 'datestring' => 'Feb 12, 2010')); + $this->assertEquals(array('December 2011', 'February 2010', 'January 2000'), array_keys($months)); + $this->assertEquals($expected, $months['February 2010']); + } } diff --git a/tests/test-vocab-data/changes.ttl b/tests/test-vocab-data/changes.ttl index d171d51dd..2ba70c3f6 100644 --- a/tests/test-vocab-data/changes.ttl +++ b/tests/test-vocab-data/changes.ttl @@ -1,4 +1,4 @@ -@prefix dc: . +@prefix dc: . @prefix changes: . @prefix meta: . @prefix owl: .