Repository: HTTP-APIs/hydrus (python)

Issue:
Change GET PUT POST DELETE requests format for collections
### I'm submitting a 
[x] bug report.

### Current Behaviour:
As discussed in the confcall, the current API semantics for operations on collections is far from being conventional:
```
/serverapi/MovieCollection/4f61070a-f647-4a65-9b0a-cbab4bcf0510/delete/3e1462f8-93ad-4a52-a371-a6fd5724c904, 1f6f1df8-788d-430a-8398-2e3302407fc0
```
Two major points:
* there is the repetition of "delete" in the uri
* multiple ids are listed with commas in the path

### Expected Behaviour:
The correct semantics should be
```
DELETE /serverapi/MovieCollection/4f61070a-f647-4a65-9b0a-cbab4bcf0510/?instances=3e1462f8-93ad-4a52-a371-a6fd5724c904,1f6f1df8-788d-430a-8398-2e3302407fc0
```
The operation method should be only defined in the request method and the list of ids to delete should be defined in a proper `instances` parameter. Every HTTP method can accept URL parameters (including PUT and DELETE).


Pull request description:
Changed endpoint format for GET,PUT, DELETE for multiple objects of Class and Collections
<!-- 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 #587

### 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 -->
The current API's endpoint format for operations on collections and deletion/insertion of multiple objects is :
```
/serverapi/MovieCollection/4f61070a-f647-4a65-9b0a-cbab4bcf0510/delete/3e1462f8-93ad-4a52-a371-a6fd5724c904, 1f6f1df8-788d-430a-8398-2e3302407fc0
```
Which isn't conventional and there is repetition of keywords like `add` and `delete` .

**Also, we had lot of code repetition in resource.py ( Items, ItemMember, ItemMembers to operate on single and multiple objects ) when most of it can be done using Item and ItemCollection resource classes with some additional params in request endpoint.**

### New expected behaviour
<!-- Describe the new code and its expected behaviour -->
Previously, if a user wanted to create multiple objects of a Class, he/she had to pass ids in endpoints as well which can be really time consuming if one wants to PUT multiple objects by making request to a Class endpoint.

## PUT (without ids):
```
/serverapi/Movie
```
Request Body: 
```
{
  "data": [
    {
      "@type": "Movie",
      "movie_name": "Avengers",
      "movie_director": "Joss Whedon"
    },
    {
      "@type": "Movie",
      "movie_name": "Justice League",
      "movie_director": "Zack Snyder"
    }
  ]
}
```

## PUT ( with ids ):
```
http://localhost:8080/serverapi/Movie?instances=id1,id2,id3
```
Response : 
```
{
    "@context": "https://raw.githubusercontent.com/HydraCG/Specifications/master/spec/latest/core/core.jsonld",
    "@type": "Status",
    "description": "Objects with ID ['98c5a86f-b324-4270-b39b-0af0b7f99d07', '99c5a86f-b324-4270-b39b-0af0b7f99d12'] successfully added",
    "iri": [
        "http://localhost:8080/serverapi/Movie/98c5a86f-b324-4270-b39b-0af0b7f99d07",
        "http://localhost:8080/serverapi/Movie/99c5a86f-b324-4270-b39b-0af0b7f99d12"
    ],
    "statusCode": 201,
    "title": "Objects successfully added"
}
```

## DELETE ( multiple Class objects):
```
http://localhost:8080/serverapi/Movie?instances=id1,id2,id3
```
Response : 
```
{
    "@context": "https://raw.githubusercontent.com/HydraCG/Specifications/master/spec/latest/core/core.jsonld",
    "@type": "Status",
    "description": "Objects with ID ['98c5a86f-b324-4270-b39b-0af0b7f99d07', '99c5a86f-b324-4270-b39b-0af0b7f99d12'] successfully deleted",
    "statusCode": 200,
    "title": "Objects successfully deleted"
}
```

## DELETE ( Multiple members from a Collection)
```
http://localhost:8080/serverapi/MovieCollection/<collection_id>?instances=id1,id2,id3
```
Response:
```
{
    "@context": "https://raw.githubusercontent.com/HydraCG/Specifications/master/spec/latest/core/core.jsonld",
    "@type": "Status",
    "description": "Objects with ID ['27a3272d-c83d-4b96-8494-2799ac5bd67e', 'ac231414-a1e6-42f2-b6f4-38ec8fbacc4d'] 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 9a47a95..3184166 100644
--- a/hydrus/app_factory.py
+++ b/hydrus/app_factory.py
@@ -19,9 +19,6 @@ def app_factory(api_name: str = "api", vocab_route: str = "vocab") -> Flask:
         Entrypoint,
         ItemCollection,
         Item,
-        Items,
-        ItemMember,
-        ItemMembers,
     )
 
     app = Flask(__name__)
@@ -51,21 +48,5 @@ def app_factory(api_name: str = "api", vocab_route: str = "vocab") -> Flask:
         ItemCollection, f"/{api_name}/<string:path>", endpoint="item_collection"
     )
     api.add_resource(Item, f"/{api_name}/<string:path>/<uuid:id_>", endpoint="item")
-    api.add_resource(
-        ItemMember,
-        f"/{api_name}/<string:path>/<uuid:collection_id_>/<uuid:id_>",
-        endpoint="member"
-    )
-    api.add_resource(
-        ItemMembers,
-        f"/{api_name}/<string:path>/<uuid:collection_id_>/delete/<int_list>",
-        endpoint="members"
-    )
-    api.add_resource(
-        Items,
-        f"/{api_name}/<string:path>/add/<int_list>",
-        f"/{api_name}/<string:path>/add",
-        f"/{api_name}/<string:path>/delete/<int_list>",
-    )
 
     return app
diff --git a/hydrus/data/crud.py b/hydrus/data/crud.py
index f991c21..f9d8ccc 100644
--- a/hydrus/data/crud.py
+++ b/hydrus/data/crud.py
@@ -149,9 +149,10 @@ def insert_multiple(
             valid/defined RDFClass but is not a dictionary neither an Abstract Property
 
     """
-    # import pdb;pdb.set_trace()
-
-    id_list = id_.split(",")
+    if id_:
+        id_list = id_.split(",")
+    else:
+        id_list = None
 
     # list to hold all the ids of inserted objects
     instance_id_list = []
@@ -164,6 +165,8 @@ def insert_multiple(
             id_of_object_ = id_list[index]
         except IndexError:
             pass
+        except TypeError:
+            pass
         inserted_object_id = insert(object_, session, id_of_object_)
         instance_id_list.append(inserted_object_id)
 
diff --git a/hydrus/resources.py b/hydrus/resources.py
index ffa5665..b0e0408 100644
--- a/hydrus/resources.py
+++ b/hydrus/resources.py
@@ -106,6 +106,13 @@ class Item(Resource):
             # Get path of the collection-class
             class_path = item_class.path
             is_collection = True
+            # To check if it's a request to GET a single instance from Collection
+            params = request.args.to_dict()
+            member_id = params.get("instances")
+            if member_id is not None:
+                return member_get_check_support(
+                    id_, member_id, class_type, class_path, path
+                )
         if checkClassOp(class_path, "GET"):
             return items_get_check_support(
                 id_, class_type, class_path, path, is_collection
@@ -165,6 +172,10 @@ class Item(Resource):
         :param path - Path for Item type( Specified in APIDoc @id) to be deleted
         """
         id_ = str(id_)
+        params = request.args.to_dict()
+        if params.get("instances"):
+            id_list = params.get("instances")
+            return items_delete_members_response(path, id_, id_list)
         collections, parsed_classes = get_collections_and_parsed_classes()
         is_collection = False
         if path in parsed_classes:
@@ -208,104 +219,26 @@ class ItemCollection(Resource):
         if not endpoint_["method"]:
             # If endpoint and PUT method is not supported in the API
             abort(endpoint_["status"])
+        # If 'instances' is available in request
+        params = request.args.to_dict()
+        object_ = json.loads(request.data.decode("utf-8"))
+        if params.get("instances") or object_.get("data"):
+            int_list = params.get("instances")
+            return items_put_response(path, int_list)
         return item_collection_put_response(path)
 
-
-class ItemMember(Resource):
-    """Handles operations(GET,DELETE) related to member of an Item(Collection).
-    (Item should be hydra:Collection)"""
-
-    @authenticate
-    def get(self, id_: str, path: str, collection_id_: str) -> Response:
-        """
-        GET object with collection_id = collection_id_
-        and member = id_ (member of a collection) from the database.
-        :param id_ : Item ID (Member)
-        :param collection_id_ : Item ID (Collection)
-        :param path : Path for Item ( Specified in APIDoc @id)
-        :return : object with member=id_ and collection_id=collection_id_
-        """
-        collection_id = str(collection_id_)
-        member_id = str(id_)
-        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, "GET"):
-            return member_get_check_support(
-                collection_id, member_id, class_type, class_path, path
-            )
-        abort(405)
-
-    @authenticate
-    def delete(self, id_: str, path: str, collection_id_: str) -> Response:
-        """
-        Delete object with id=id_ from database.
-        :param id_ - ID of Item to be deleted
-        :param path - Path for Item type( Specified in APIDoc @id) to be deleted
-        """
-        collection_id = str(collection_id_)
-        member_id = str(id_)
-        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"):
-            return member_delete_check_support(
-                collection_id, member_id, class_type, path
-            )
-        abort(405)
-
-
-class Items(Resource):
-    """Handles operations(PUT,DELETE) related to multiple objects.
-    (Item should be hydra:Class)"""
-
     @authenticate
-    def put(self, path, int_list="") -> Response:
-        """
-        To insert multiple objects into the database
-        :param path: endpoint
-        :param int_list: Optional String containing ',' separated ID's
-        :return:
-        """
-        endpoint_ = checkEndpoint("PUT", path)
-        if not endpoint_["method"]:
-            # If endpoint and PUT method is not supported in the API
-            abort(endpoint_["status"])
-        return items_put_response(path, int_list)
-
-    @authenticate
-    def delete(self, path, int_list):
+    def delete(self, path):
         """
         To delete multiple objects
         :param path: endpoints
-        :param int_list: Optional String containing ',' separated ID's
         :return:
         """
-        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)
+        params = request.args.to_dict()
+        if params.get("instances"):
+            int_list = params.get("instances")
+            return items_delete_response(path, int_list)
+        abort(405)
 
 
 class Contexts(Resource):


Test patch:
diff --git a/tests/functional/test_app.py b/tests/functional/test_app.py
index b595c67..5634248 100644
--- a/tests/functional/test_app.py
+++ b/tests/functional/test_app.py
@@ -319,7 +319,7 @@ class TestApp():
                         ids = f'{uuid.uuid4()},'
                     data_['data'] = objects
                     if 'PUT' in class_methods:
-                        put_response = test_app_client.put(f'{endpoints[endpoint]}/add/{ids}',
+                        put_response = test_app_client.put(f'{endpoints[endpoint]}?instances={ids}',
                                                            data=json.dumps(data_))
                         assert put_response.status_code == 201
                         assert isinstance(put_response.json['iri'], list)
@@ -436,7 +436,7 @@ class TestApp():
                 if 'GET' in collection_methods:
                     for member in dummy_object['members']:
                         member_id = member['@id'].split('/')[-1]
-                        get_response = test_app_client.get(f'{collection_endpoint}/{member_id}')
+                        get_response = test_app_client.get(f'{collection_endpoint}?instances={member_id}')
                         assert get_response.status_code == 200
 
     def test_Collections_member_DELETE(self, test_app_client, constants, doc):
@@ -458,7 +458,7 @@ class TestApp():
                 if 'DELETE' in collection_methods:
                     for member in dummy_object['members']:
                         member_id = member['@id'].split('/')[-1]
-                        full_endpoint = f'{collection_endpoint}/{member_id}'
+                        full_endpoint = f'{collection_endpoint}?instances={member_id}'
                         delete_response = test_app_client.delete(full_endpoint)
                         assert delete_response.status_code == 200
 
@@ -480,7 +480,7 @@ class TestApp():
                 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}'
+                    full_endpoint = f'{collection_endpoint}?instances={ids_list}'
                     delete_response = test_app_client.delete(full_endpoint)
                     assert delete_response.status_code == 200