Repository: HTTP-APIs/hydrus (python)

Issue:
Create new endpoints to delete multiple members from a Collection
### I'm submitting a 
- [ ] bug report.
- [x] feature request.

### Current Behaviour:
<!-- Describe about the bug -->
To make it easier for a user to delete multiple members from a Collection we can add an
endpoint that would allow us to do this. Currently, we do have endpoints to insert multiple
objects or even delete multiple objects by making a single request to a Class endpoint, but
no such endpoint is there for Collections.

### Expected Behaviour:
<!-- Describe what will happen if bug is removed -->
An endpoint can be created that would allow us to make a DELETE request with
collection_id and multiple members ids in ids_list as parameter.
`<api-name>/<resource_name>/<collection-id>/delete/<ids_list> `
### Steps to reproduce:
<!-- If you can then please provide the steps to reproduce the bug -->

### Snapshot:
<!-- If you can then please provide the screenshot of the issue you are facing -->

### Environment:
<!-- Please provide the following environment details --> 
* python version
* pip version
* OS details

### Do you want to work on this issue?
<!-- yes/no -->
Yes

Pull request:
Added endpoint to delete multiple members from a Collection.
<!-- Please create (if there is not one yet) a issue before sending a PR -->
<!-- Add issue number (Eg: fixes #123) -->
<!-- Always provide changes in existing tests or new tests -->

Fixes #579 

### Checklist
- [x] My branch is up-to-date with upstream/develop branch.
- [x] Everything works and tested for Python 3.6.0 and above.

### Current behaviour
<!-- Describe the code you are going to change and its behaviour -->
Currently, we do have endpoints to insert multiple
objects or even delete multiple objects by making a single request to a Class endpoint, but
no such endpoint is there to delete multiple members from a Collection.
### New expected behaviour
<!-- Describe the new code and its expected behaviour -->
Create an endpoint to make a DELETE request with
collection_id and multiple members ids in ids_list as parameter.
`<api-name>/<resource_name>/<collection-id>/delete/<ids_list>`

DELETE : 
Example endpoint : 
```
/serverapi/MovieCollection/4f61070a-f647-4a65-9b0a-cbab4bcf0510/delete/3e1462f8-93ad-4a52-a371-a6fd5724c904, 1f6f1df8-788d-430a-8398-2e3302407fc0
```

```
{
    "@context": "https://raw.githubusercontent.com/HydraCG/Specifications/master/spec/latest/core/core.jsonld",
    "@type": "Status",
    "description": "Objects with ID ['3e1462f8-93ad-4a52-a371-a6fd5724c904', '1f6f1df8-788d-430a-8398-2e3302407fc0'] successfully deleted",
    "statusCode": 200,
    "title": "Objects successfully deleted"
}
```
### Change logs

<!-- #### Added -->
<!-- Edit these points below to describe the new features added with this PR -->
<!-- - Feature 1 -->
<!-- - Feature 2 -->


<!-- #### Changed -->
<!-- Edit these points below to describe the changes made in existing functionality with this PR -->
<!-- - Change 1 -->
<!-- - Change 1 -->


<!-- #### Fixed -->
<!-- Edit these points below to describe the bug fixes made with this PR -->
<!-- - Bug 1 -->


<!-- #### Removed -->
<!-- Edit these points below to describe the removed features with this PR -->
<!-- - Deprecated feature 1 -->


Patch:
diff --git a/hydrus/app_factory.py b/hydrus/app_factory.py
index 6e412d5..9a47a95 100644
--- a/hydrus/app_factory.py
+++ b/hydrus/app_factory.py
@@ -21,6 +21,7 @@ def app_factory(api_name: str = "api", vocab_route: str = "vocab") -> Flask:
         Item,
         Items,
         ItemMember,
+        ItemMembers,
     )
 
     app = Flask(__name__)
@@ -53,7 +54,12 @@ def app_factory(api_name: str = "api", vocab_route: str = "vocab") -> Flask:
     api.add_resource(
         ItemMember,
         f"/{api_name}/<string:path>/<uuid:collection_id_>/<uuid:id_>",
-        endpoint="member",
+        endpoint="member"
+    )
+    api.add_resource(
+        ItemMembers,
+        f"/{api_name}/<string:path>/<uuid:collection_id_>/delete/<int_list>",
+        endpoint="members"
     )
     api.add_resource(
         Items,
diff --git a/hydrus/data/crud.py b/hydrus/data/crud.py
index a31f8ed..f991c21 100644
--- a/hydrus/data/crud.py
+++ b/hydrus/data/crud.py
@@ -407,7 +407,7 @@ def delete_member(
 
     Raises:
         ClassNotFound: If `type_` does not represent a valid/defined RDFClass.
-        InstanceNotFound: If no instace of type `type_` with id `id_` exists.
+        MemberInstanceNotFound: If no instace of type `type_` with id `id_` exists.
 
     """
     query_info = {
@@ -418,6 +418,29 @@ def delete_member(
     delete_collection_member(query_info, session)
 
 
+def delete_multiple_members(
+        collection_id_: str,
+        id_: List[str],
+        type_: str,
+        session: scoped_session) -> None:
+    """
+    To delete multiple members in a single request
+    :param collection_id_: ID of the collection
+    :param id_: list of ids of members to be deleted
+    :param type_: type of object to be deleted
+    :param session: sqlalchemy scoped session
+
+    Raises:
+        ClassNotFound: If `type_` does not represent a valid/defined RDFClass.
+        MemberInstanceNotFound: If any instance with type 'type_' and any id in 'id_' list
+            does not exist.
+
+    """
+    id_list = id_.split(',')
+    for member_id_ in id_list:
+        delete_member(collection_id_, member_id_, type_, session)
+
+
 def insert_modification_record(
     method: str, resource_url: str, session: scoped_session
 ) -> int:
diff --git a/hydrus/data/helpers/items_helpers.py b/hydrus/data/helpers/items_helpers.py
index d70b7cf..a4f8280 100644
--- a/hydrus/data/helpers/items_helpers.py
+++ b/hydrus/data/helpers/items_helpers.py
@@ -156,3 +156,46 @@ def items_delete_response(path: str, int_list="") -> Response:
             return error_response(error)
 
     abort(405)
+
+
+def items_delete_members_response(path: str, collection_id_: str, int_list="") -> Response:
+    """
+    Handles DELETE operation to insert multiple items.
+
+    :param path: Path for Item Collection
+    :type path: str
+    :param collection_id_: ID of the collection
+    :type collection_id_: str
+    :param int_list: Optional String containing ',' separated ID's
+    :type int_list: List
+    :return: Appropriate response for the DELETE operation on multiple items.
+    :rtype: Response
+    """
+    collections, parsed_classes = get_collections_and_parsed_classes()
+    if path in parsed_classes:
+            abort(405)
+    if path in collections:
+        item_class = collections[path]["collection"]
+        class_type = item_class.name
+        # Get path of the collection-class
+        class_path = item_class.path
+    if checkClassOp(class_path, "DELETE"):
+        # Check if class_type supports PUT operation
+        try:
+            # Delete the Item with ID == id_
+            crud.delete_multiple_members(collection_id_,
+                                         int_list,
+                                         class_type,
+                                         session=get_session())
+            id_list = int_list.split(',')
+            status_description = f"Objects with ID {id_list} successfully deleted"
+            status = HydraStatus(code=200,
+                                 title="Objects successfully deleted",
+                                 desc=status_description)
+            return set_response_headers(jsonify(status.generate()))
+
+        except (ClassNotFound, InstanceNotFound) as e:
+            error = e.get_HTTP()
+            return error_response(error)
+
+    abort(405)
diff --git a/hydrus/resources.py b/hydrus/resources.py
index a0c1f97..ffa5665 100644
--- a/hydrus/resources.py
+++ b/hydrus/resources.py
@@ -46,7 +46,11 @@ from hydrus.data.helpers.item_collection_helpers import (
     item_collection_get_response,
     item_collection_put_response,
 )
-from hydrus.data.helpers.items_helpers import items_put_response, items_delete_response
+from hydrus.data.helpers.items_helpers import (
+    items_put_response,
+    items_delete_response,
+    items_delete_members_response,
+)
 
 
 class Index(Resource):
@@ -290,6 +294,20 @@ class Items(Resource):
         return items_delete_response(path, int_list)
 
 
+class ItemMembers(Resource):
+    @authenticate
+    def delete(self, path, collection_id_, int_list):
+        """
+        To delete multiple members from a Collection
+        :param path: endpoints
+        :param collection_id_: ID of the collection
+        :param int_list: Optional String containing ',' separated ID's
+        :return:
+        """
+        collection_id_ = str(collection_id_)
+        return items_delete_members_response(path, collection_id_, int_list)
+
+
 class Contexts(Resource):
     """Dynamically generated contexts."""
 


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 9c6aa83..2544968 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -92,6 +92,20 @@ def gen_dummy_object(class_title, doc):
             return object_
 
 
+def get_ids_list(dummy_object):
+    """
+    Takes a dummy object for Collection and returns ids_list separated by ','.
+    :param dummy_object: dummy_object created for collection.
+    :return: A string of ids of members separated by ','.
+    """
+    id_list = []
+    for member in dummy_object["members"]:
+        id = member["@id"].split('/')[-1]
+        id_list.append(id)
+    ids = ','.join(id_list)
+    return ids
+
+
 @pytest.fixture(scope='module')
 def constants():
     """
diff --git a/tests/functional/test_app.py b/tests/functional/test_app.py
index b815458..b595c67 100644
--- a/tests/functional/test_app.py
+++ b/tests/functional/test_app.py
@@ -10,7 +10,7 @@ import uuid
 import pytest
 from hydra_python_core.doc_writer import HydraLink, DocUrl
 
-from tests.conftest import gen_dummy_object
+from tests.conftest import gen_dummy_object, get_ids_list
 from hydrus.utils import get_doc
 
 
@@ -462,6 +462,28 @@ class TestApp():
                         delete_response = test_app_client.delete(full_endpoint)
                         assert delete_response.status_code == 200
 
+    def test_Collections_member_DELETE_multiple(self, capsys, test_app_client, constants, doc):
+        """Test endpoint to delete member from a collection."""
+        API_NAME = constants['API_NAME']
+        index = test_app_client.get(f'/{API_NAME}')
+        assert index.status_code == 200
+        endpoints = json.loads(index.data.decode('utf-8'))
+        for endpoint in endpoints['collections']:
+            collection_name = '/'.join(endpoint['@id'].split(f'/{API_NAME}/')[1:])
+            collection = doc.collections[collection_name]['collection']
+            collection_methods = [x.method for x in collection.supportedOperation]
+            if 'PUT' in collection_methods:
+                dummy_object = gen_dummy_object(collection.name, doc)
+                good_response_put = test_app_client.put(endpoint['@id'],
+                                                        data=json.dumps(dummy_object))
+                assert good_response_put.status_code == 201
+                collection_endpoint = good_response_put.location
+                if 'DELETE' in collection_methods:
+                    ids_list = get_ids_list(dummy_object)
+                    full_endpoint = f'{collection_endpoint}/delete/{ids_list}'
+                    delete_response = test_app_client.delete(full_endpoint)
+                    assert delete_response.status_code == 200
+
     def test_IriTemplate(self, test_app_client, constants, doc):
         """Test structure of IriTemplates attached to parsed classes"""
         API_NAME = constants['API_NAME']