Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(admin): add list child node deletion route (DEV-729) (#2064)
* add test data

* fix tests

* fix more tests

* add delete list route messages, responder, route and sparql

* add tests

* fix formatting

* fix lists messages

* add docs

* fix formatting

* review changes

* simplify route + fix typos
  • Loading branch information
mpro7 committed May 24, 2022
1 parent c7ad29f commit 179ad19
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 21 deletions.
30 changes: 30 additions & 0 deletions docs/03-apis/api-admin/lists.md
Expand Up @@ -15,6 +15,8 @@ If IRI of the child node is given, return the node with its immediate children
- `GET: /admin/lists/infos/<listIri>` : return list information (without children)
- `GET: /admin/lists/nodes/<nodeIri>` : return list node information (without children)
- `GET: /admin/lists/<listIri>/info` : return list basic information (without children)
- `GET: /admin/lists/candelete/<listItemIri>` : check if list or its node is unused and can be deleted


- `POST: /admin/lists` : create new list
- `POST: /admin/lists/<parentNodeIri>` : create new child node under the supplied parent node IRI
Expand All @@ -27,6 +29,7 @@ If IRI of the child node is given, return the node with its immediate children
parent node

- `DELETE: /admin/lists/<listItemIri>` : delete a list (i.e. root node) or a child node and all its children, if not used
- `DELETE: /admin/lists/comments/<nodeIri>` : delete comments of a node (child only)

## List Item Operations

Expand Down Expand Up @@ -61,6 +64,21 @@ and all its children
- Return list (or node) basic information, `listinfo` (or `nodeinfo`), without its children
- GET: `/admin/lists/<listIri>/info`

### Check if list node is unused and can be deleted

- Required permission: none
- GET: `/admin/lists/candelete/<listItemIri>`
- Return simple JSON that confirms if the list node can be deleted

```json
{
"canDeleteList": true,
"listIri": "http://rdfh.ch/lists/0801/xxx"
}
```

List (root node or child node with all its children) can be deleted only if it (or one of its children) is not used.

### Create new list

- Required permission: SystemAdmin / ProjectAdmin
Expand Down Expand Up @@ -338,3 +356,15 @@ remaining child nodes with respect to the position of the deleted node.
- If the IRI of a child node is given, the updated parent node is returned.

- Delete `/admin/lists/<listItemIri>`

### Delete child node comments

Performing a DELETE request to route `/admin/lists/comments/<nodeIri>` deletes the comments of that node.
As a response sipmle JSON is returned:

```json
{
"commentsDeleted": true,
"nodeIri": "http://rdfh.ch/lists/0801/xxx"
}
```
36 changes: 35 additions & 1 deletion test_data/all_data/anything-data.ttl
Expand Up @@ -2196,4 +2196,38 @@
knora-base:hasRootNode <http://rdfh.ch/lists/0001/notUsedList> ;
knora-base:listNodePosition 0 ;
rdfs:label "child of node 3"@en .


<http://rdfh.ch/lists/0001/testList>
a knora-base:ListNode ;
knora-base:isRootNode true ;
knora-base:listNodeName "Test list root for comments" ;
rdfs:label "Test list root label"@en ;
rdfs:comment "Test list root comment"@en ;
knora-base:attachedToProject <http://rdfh.ch/projects/0001> ;
knora-base:hasSubListNode <http://rdfh.ch/lists/0001/testList01>,
<http://rdfh.ch/lists/0001/testList02>,
<http://rdfh.ch/lists/0001/testList03> .

<http://rdfh.ch/lists/0001/testList01>
a knora-base:ListNode ;
knora-base:listNodeName "Test list node 01 with one comment" ;
knora-base:hasRootNode <http://rdfh.ch/lists/0001/testList> ;
knora-base:listNodePosition 0 ;
rdfs:label "Test list node 01"@en ;
rdfs:comment "Test list child node comment"@en .

<http://rdfh.ch/lists/0001/testList02>
a knora-base:ListNode ;
knora-base:listNodeName "Test list node 02 with two comments" ;
knora-base:hasRootNode <http://rdfh.ch/lists/0001/testList> ;
knora-base:listNodePosition 0 ;
rdfs:label "Test list node 02"@en ;
rdfs:comment "Test list child node comment 01"@en ;
rdfs:comment "Test list child node comment 02"@en .

<http://rdfh.ch/lists/0001/testList03>
a knora-base:ListNode ;
knora-base:listNodeName "Test list node 03 w/o comments" ;
knora-base:hasRootNode <http://rdfh.ch/lists/0001/testList> ;
knora-base:listNodePosition 0 ;
rdfs:label "Test list node 03"@en .
Expand Up @@ -22,9 +22,9 @@ import org.knora.webapi.messages.store.triplestoremessages.TriplestoreJsonProtoc
import spray.json._

import java.util.UUID
import org.knora.webapi.messages.admin.responder.valueObjects.Comments

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// API requests
/////////////// API requests

/**
* Represents an API request payload that asks the Knora API server to create a new list root node.
Expand Down Expand Up @@ -330,7 +330,7 @@ case class ListItemDeleteRequestADM(
) extends ListsResponderRequestADM

/**
* Request checks if a list is unused and can be deleted. A successful response will be a [[CanDeleteListResponseADM]]
* Requests checks if a list is unused and can be deleted. A successful response will be a [[CanDeleteListResponseADM]]
*
* @param iri the IRI of the list node (root or child).
* @param featureFactoryConfig the feature factory configuration.
Expand All @@ -339,8 +339,45 @@ case class ListItemDeleteRequestADM(
case class CanDeleteListRequestADM(iri: IRI, featureFactoryConfig: FeatureFactoryConfig, requestingUser: UserADM)
extends ListsResponderRequestADM

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Responses
/**
* Requests deletion of all list node comments. A successful response will be a [[ListNodeCommentsDeleteADM]]
*
* @param iri the IRI of the list node (root or child).
* @param featureFactoryConfig the feature factory configuration.
* @param requestingUser the user making the request.
*/
case class ListNodeCommentsDeleteRequestADM(
iri: IRI,
featureFactoryConfig: FeatureFactoryConfig,
requestingUser: UserADM
) extends ListsResponderRequestADM

///////////////////////// Responses

/**
* Responds to deletion of list node's comments by returning a success message.
*
* @param nodeIri the IRI of the list that comments are deleted.
* @param commentsDeleted contains a boolean value if comments were deleted.
*/
case class ListNodeCommentsDeleteResponseADM(nodeIri: IRI, commentsDeleted: Boolean)
extends KnoraResponseADM
with ListADMJsonProtocol {
def toJsValue: JsValue = ListNodeCommentsDeleteResponseADMFormat.write(this)
}

/**
* Returns an information if node can be deleted (none of its nodes is used in data).
*
* @param iri the IRI of the list that is checked.
* @param canDeleteList contains a boolean value if list node can be deleted.
*/
case class CanDeleteListResponseADM(listIri: IRI, canDeleteList: Boolean)
extends KnoraResponseADM
with ListADMJsonProtocol {

def toJsValue: JsValue = canDeleteListResponseADMFormat.write(this)
}

/**
* Represents a sequence of list info nodes.
Expand Down Expand Up @@ -433,16 +470,6 @@ case class ChildNodeDeleteResponseADM(node: ListNodeADM) extends ListItemDeleteR
def toJsValue: JsValue = listNodeDeleteResponseADMFormat.write(this)
}

/**
* Checks if a list can be deleted (none of its nodes is used in data).
*
* @param iri the IRI of the list that is checked.
*/
case class CanDeleteListResponseADM(listIri: IRI, canDeleteList: Boolean) extends ListItemDeleteResponseADM {

def toJsValue: JsValue = canDeleteListResponseADMFormat.write(this)
}

/**
* Responds to change of a child node's position by returning its parent node together with list of its children.
*
Expand Down Expand Up @@ -1339,4 +1366,6 @@ trait ListADMJsonProtocol extends SprayJsonSupport with DefaultJsonProtocol with
jsonFormat(ListDeleteResponseADM, "iri", "deleted")
implicit val canDeleteListResponseADMFormat: RootJsonFormat[CanDeleteListResponseADM] =
jsonFormat(CanDeleteListResponseADM, "listIri", "canDeleteList")
implicit val ListNodeCommentsDeleteResponseADMFormat: RootJsonFormat[ListNodeCommentsDeleteResponseADM] =
jsonFormat(ListNodeCommentsDeleteResponseADM, "nodeIri", "commentsDeleted")
}
Expand Up @@ -92,6 +92,8 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde
deleteListItemRequestADM(nodeIri, featureFactoryConfig, requestingUser, apiRequestID)
case CanDeleteListRequestADM(iri, featureFactoryConfig, requestingUser) =>
canDeleteListRequestADM(iri)
case ListNodeCommentsDeleteRequestADM(iri, featureFactoryConfig, requestingUser) =>
deleteListNodeCommentsADM(iri, featureFactoryConfig, requestingUser)
case other => handleUnexpectedMessage(other, log, this.getClass.getName)
}

Expand Down Expand Up @@ -1830,6 +1832,57 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde

} yield CanDeleteListResponseADM(iri, canDelete)

/**
* Deletes all comments from requested list node (only child).
*/
private def deleteListNodeCommentsADM(
iri: IRI,
featureFactoryConfig: FeatureFactoryConfig,
requestingUser: UserADM
): Future[ListNodeCommentsDeleteResponseADM] =
for {
node <- listNodeInfoGetADM(
nodeIri = iri,
featureFactoryConfig = featureFactoryConfig,
requestingUser = KnoraSystemInstances.Users.SystemUser
)

doesNodeHaveComments = node.get.getComments.stringLiterals.length > 0

_ = if (!doesNodeHaveComments) {
throw BadRequestException(s"Nothing to delete. Node $iri does not have comments.")
}

isRootNode =
node match {
case Some(_: ListRootNodeInfoADM) => true
case Some(_: ListChildNodeInfoADM) => false
case _ => throw InconsistentRepositoryDataException("Bad data. List node expected.")
}

_ = if (isRootNode) {
throw BadRequestException("Root node comments cannot be deleted.")
}

projectIri <- getProjectIriFromNode(iri, featureFactoryConfig)
namedGraph <- getDataNamedGraph(projectIri, featureFactoryConfig)

sparqlQuery <-
Future(
org.knora.webapi.messages.twirl.queries.sparql.admin.txt
.deleteListNodeComments(
namedGraph = namedGraph,
nodeIri = iri,
isRootNode = isRootNode
)
.toString()
)

_: SparqlUpdateResponse <- (storeManager ? SparqlUpdateRequest(sparqlQuery))
.mapTo[SparqlUpdateResponse]

} yield ListNodeCommentsDeleteResponseADM(iri, !isRootNode)

/**
* Delete a node (root or child). If a root node is given, check for its usage in data and ontology. If not used,
* delete the list and return a confirmation message.
Expand Down
Expand Up @@ -39,7 +39,8 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData)

def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route =
deleteListItem(featureFactoryConfig) ~
canDeleteList(featureFactoryConfig)
canDeleteList(featureFactoryConfig) ~
deleteListNodeComments(featureFactoryConfig)

/* delete list (i.e. root node) or a child node which should also delete its children */
private def deleteListItem(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath / Segment) { iri =>
Expand Down Expand Up @@ -96,4 +97,32 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData)
)
}
}

/**
* Deletes all comments from requested list node (only child).
*/
private def deleteListNodeComments(featureFactoryConfig: FeatureFactoryConfig): Route =
path(ListsBasePath / "comments" / Segment) { iri =>
delete { requestContext =>
val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid list IRI: $iri"))

val requestMessage: Future[ListNodeCommentsDeleteRequestADM] =
for {
requestingUser <- getUserADM(requestContext, featureFactoryConfig)
} yield ListNodeCommentsDeleteRequestADM(
iri = listIri,
featureFactoryConfig = featureFactoryConfig,
requestingUser = requestingUser
)

RouteUtilADM.runJsonRoute(
requestMessageF = requestMessage,
requestContext = requestContext,
featureFactoryConfig = featureFactoryConfig,
settings = settings,
responderManager = responderManager,
log = log
)
}
}
}
@@ -0,0 +1,32 @@
@*
* Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
* SPDX-License-Identifier: Apache-2.0
*@

@import org.knora.webapi.IRI

@**
* Deletes a list node comment.
*
* @param namedGraph the named graph to update.
* @param nodeIri the IRI of the list node to update.
* @param isRootNode flag to identify node type.
*@
@(namedGraph: IRI,
nodeIri: IRI,
isRootNode: Boolean)

PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX knora-base: <http://www.knora.org/ontology/knora-base#>

DELETE {
GRAPH <@namedGraph> {
<@nodeIri> rdfs:comment ?comments .
}
}

WHERE {
GRAPH <@namedGraph> {
<@nodeIri> rdfs:comment ?comments .
}
}
Expand Up @@ -106,7 +106,7 @@ class OldListsRouteADMFeatureE2ESpec

// log.debug("lists: {}", lists)

lists.size should be(8)
lists.size should be(9)
clientTestDataCollector.addFile(
TestDataFileContent(
filePath = TestDataFilePath(
Expand Down Expand Up @@ -161,7 +161,7 @@ class OldListsRouteADMFeatureE2ESpec

// log.debug("lists: {}", lists)

lists.size should be(3)
lists.size should be(4)

clientTestDataCollector.addFile(
TestDataFileContent(
Expand Down

0 comments on commit 179ad19

Please sign in to comment.