From 179ad19bc25637e73d3e850299877600638fd57f Mon Sep 17 00:00:00 2001 From: Marcin Procyk Date: Tue, 24 May 2022 10:49:35 +0200 Subject: [PATCH] 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 --- docs/03-apis/api-admin/lists.md | 30 ++++++++++ test_data/all_data/anything-data.ttl | 36 ++++++++++- .../listsmessages/ListsMessagesADM.scala | 59 ++++++++++++++----- .../responders/admin/ListsResponderADM.scala | 53 +++++++++++++++++ .../admin/lists/DeleteListItemsRouteADM.scala | 31 +++++++++- .../admin/deleteListNodeComments.scala.txt | 32 ++++++++++ .../OldListsRouteADMFeatureE2ESpec.scala | 4 +- .../admin/ListsResponderADMSpec.scala | 56 +++++++++++++++++- 8 files changed, 280 insertions(+), 21 deletions(-) create mode 100644 webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/deleteListNodeComments.scala.txt diff --git a/docs/03-apis/api-admin/lists.md b/docs/03-apis/api-admin/lists.md index 366249ef32..89ae20b717 100644 --- a/docs/03-apis/api-admin/lists.md +++ b/docs/03-apis/api-admin/lists.md @@ -15,6 +15,8 @@ If IRI of the child node is given, return the node with its immediate children - `GET: /admin/lists/infos/` : return list information (without children) - `GET: /admin/lists/nodes/` : return list node information (without children) - `GET: /admin/lists//info` : return list basic information (without children) +- `GET: /admin/lists/candelete/` : check if list or its node is unused and can be deleted + - `POST: /admin/lists` : create new list - `POST: /admin/lists/` : create new child node under the supplied parent node IRI @@ -27,6 +29,7 @@ If IRI of the child node is given, return the node with its immediate children parent node - `DELETE: /admin/lists/` : delete a list (i.e. root node) or a child node and all its children, if not used +- `DELETE: /admin/lists/comments/` : delete comments of a node (child only) ## List Item Operations @@ -61,6 +64,21 @@ and all its children - Return list (or node) basic information, `listinfo` (or `nodeinfo`), without its children - GET: `/admin/lists//info` +### Check if list node is unused and can be deleted + +- Required permission: none +- GET: `/admin/lists/candelete/` +- 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 @@ -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/` + +### Delete child node comments + +Performing a DELETE request to route `/admin/lists/comments/` deletes the comments of that node. +As a response sipmle JSON is returned: + +```json +{ + "commentsDeleted": true, + "nodeIri": "http://rdfh.ch/lists/0801/xxx" +} +``` diff --git a/test_data/all_data/anything-data.ttl b/test_data/all_data/anything-data.ttl index 36825135b2..d107115bef 100644 --- a/test_data/all_data/anything-data.ttl +++ b/test_data/all_data/anything-data.ttl @@ -2196,4 +2196,38 @@ knora-base:hasRootNode ; knora-base:listNodePosition 0 ; rdfs:label "child of node 3"@en . - \ No newline at end of file + + + 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 ; + knora-base:hasSubListNode , + , + . + + + a knora-base:ListNode ; + knora-base:listNodeName "Test list node 01 with one comment" ; + knora-base:hasRootNode ; + knora-base:listNodePosition 0 ; + rdfs:label "Test list node 01"@en ; + rdfs:comment "Test list child node comment"@en . + + + a knora-base:ListNode ; + knora-base:listNodeName "Test list node 02 with two comments" ; + knora-base:hasRootNode ; + 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 . + + + a knora-base:ListNode ; + knora-base:listNodeName "Test list node 03 w/o comments" ; + knora-base:hasRootNode ; + knora-base:listNodePosition 0 ; + rdfs:label "Test list node 03"@en . diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala index 153aadfd02..9526a8e544 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala @@ -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. @@ -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. @@ -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. @@ -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. * @@ -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") } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala index 8e2aa78c48..93af1e6cdd 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala @@ -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) } @@ -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. diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala index 62759ed44d..7f6f03d169 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala @@ -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 => @@ -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 + ) + } + } } diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/deleteListNodeComments.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/deleteListNodeComments.scala.txt new file mode 100644 index 0000000000..89a114ad10 --- /dev/null +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/deleteListNodeComments.scala.txt @@ -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: +PREFIX knora-base: + +DELETE { + GRAPH <@namedGraph> { + <@nodeIri> rdfs:comment ?comments . + } +} + +WHERE { + GRAPH <@namedGraph> { + <@nodeIri> rdfs:comment ?comments . + } +} diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/OldListsRouteADMFeatureE2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/OldListsRouteADMFeatureE2ESpec.scala index c02fd74aa9..d1111b8317 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/OldListsRouteADMFeatureE2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/OldListsRouteADMFeatureE2ESpec.scala @@ -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( @@ -161,7 +161,7 @@ class OldListsRouteADMFeatureE2ESpec // log.debug("lists: {}", lists) - lists.size should be(3) + lists.size should be(4) clientTestDataCollector.addFile( TestDataFileContent( diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala index 8c52d6ef96..643b6e9e92 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala @@ -70,7 +70,7 @@ class ListsResponderADMSpec extends CoreSpec(ListsResponderADMSpec.config) with val received: ListsGetResponseADM = expectMsgType[ListsGetResponseADM](timeout) - received.lists.size should be(8) + received.lists.size should be(9) } "return all lists belonging to the images project" in { @@ -98,7 +98,7 @@ class ListsResponderADMSpec extends CoreSpec(ListsResponderADMSpec.config) with // log.debug("received: " + received) - received.lists.size should be(3) + received.lists.size should be(4) } "return basic list information (anything list)" in { @@ -1021,5 +1021,57 @@ class ListsResponderADMSpec extends CoreSpec(ListsResponderADMSpec.config) with response.canDeleteList should be(true) } } + + "used to delete list node comments" should { + "do not delete a comment of root list node" in { + val nodeIri = "http://rdfh.ch/lists/0001/testList" + responderManager ! ListNodeCommentsDeleteRequestADM( + iri = nodeIri, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = SharedTestDataADM.anythingAdminUser + ) + expectMsg( + Failure(BadRequestException("Root node comments cannot be deleted.")) + ) + } + + "delete all comments of child node that contains just one comment" in { + val nodeIri = "http://rdfh.ch/lists/0001/testList01" + responderManager ! ListNodeCommentsDeleteRequestADM( + iri = nodeIri, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = SharedTestDataADM.anythingAdminUser + ) + val response: ListNodeCommentsDeleteResponseADM = + expectMsgType[ListNodeCommentsDeleteResponseADM](timeout) + response.nodeIri should be(nodeIri) + response.commentsDeleted should be(true) + } + + "delete all comments of child node that contains more than one comment" in { + val nodeIri = "http://rdfh.ch/lists/0001/testList02" + responderManager ! ListNodeCommentsDeleteRequestADM( + iri = nodeIri, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = SharedTestDataADM.anythingAdminUser + ) + val response: ListNodeCommentsDeleteResponseADM = + expectMsgType[ListNodeCommentsDeleteResponseADM](timeout) + response.nodeIri should be(nodeIri) + response.commentsDeleted should be(true) + } + + "if reqested list does not have comments, inform there is no comments to delete" in { + val nodeIri = "http://rdfh.ch/lists/0001/testList03" + responderManager ! ListNodeCommentsDeleteRequestADM( + iri = nodeIri, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = SharedTestDataADM.anythingAdminUser + ) + expectMsg( + Failure(BadRequestException(s"Nothing to delete. Node $nodeIri does not have comments.")) + ) + } + } } }