diff --git a/dspace_statistics_api/app.py b/dspace_statistics_api/app.py index 478dc02..32a9bb3 100644 --- a/dspace_statistics_api/app.py +++ b/dspace_statistics_api/app.py @@ -1,6 +1,7 @@ import falcon from .database import DatabaseManager +from .util import validate_items_post_parameters class RootResource: @@ -55,74 +56,29 @@ class AllItemsResource: resp.media = message + @falcon.before(validate_items_post_parameters) def on_post(self, req, resp): """Handles POST requests""" - import json from .items import get_views from .items import get_downloads - from .util import is_valid_date - - # Only attempt to read the POSTed request if its length is not 0 (or - # rather, in the Python sense, if length is not a False-y value). - if req.content_length: - doc = json.load(req.bounded_stream) - else: - raise falcon.HTTPBadRequest( - title="Invalid request", description=f"Request body is empty." - ) - - # Parse date parameters from request body (will raise an HTTPBadRequest - # from is_valid_date() if any parameters are invalid) - req_dateFrom = ( - doc["dateFrom"] - if "dateFrom" in doc and is_valid_date(doc["dateFrom"]) - else None - ) - req_dateTo = ( - doc["dateTo"] if "dateTo" in doc and is_valid_date(doc["dateTo"]) else None - ) # Build the Solr date string, ie: [* TO *] - if req_dateFrom and req_dateTo: - solr_date_string = f"[{req_dateFrom} TO {req_dateTo}]" - elif not req_dateFrom and req_dateTo: - solr_date_string = f"[* TO {req_dateTo}]" - elif req_dateFrom and not req_dateTo: - solr_date_string = f"[{req_dateFrom} TO *]" + if req.context.dateFrom and req.context.dateTo: + solr_date_string = f"[{req.context.dateFrom} TO {req.context.dateTo}]" + elif not req.context.dateFrom and req.context.dateTo: + solr_date_string = f"[* TO {req.context.dateTo}]" + elif req.context.dateFrom and not req.context.dateTo: + solr_date_string = f"[{req.context.dateFrom} TO *]" else: solr_date_string = "[* TO *]" - # Parse the limit parameter from the POST request body - req_limit = doc["limit"] if "limit" in doc else 100 - if not isinstance(req_limit, int) or req_limit < 0 or req_limit > 100: - raise falcon.HTTPBadRequest( - title="Invalid parameter", - description=f'The "limit" parameter is invalid. The value must be an integer between 0 and 100.', - ) - - # Parse the page parameter from the POST request body - req_page = doc["page"] if "page" in doc else 0 - if not isinstance(req_page, int) or req_page < 0: - raise falcon.HTTPBadRequest( - title="Invalid parameter", - description=f'The "page" parameter is invalid. The value must be at least 0.', - ) - - # Parse the list of items from the POST request body - req_items = doc["items"] if "items" in doc else list() - if not isinstance(req_items, list) or len(req_items) == 0: - raise falcon.HTTPBadRequest( - title="Invalid parameter", - description=f'The "items" parameter is invalid. The value must be a comma-separated list of item UUIDs.', - ) - # Helper variables to make working with pages/items/results easier and # to make the code easier to understand - number_of_items: int = len(req_items) - pages: int = int(number_of_items / req_limit) - first_item: int = req_page * req_limit - last_item: int = first_item + req_limit + number_of_items: int = len(req.context.items) + pages: int = int(number_of_items / req.context.limit) + first_item: int = req.context.page * req.context.limit + last_item: int = first_item + req.context.limit # Get a subset of the POSTed items based on our limit. Note that Python # list slicing and indexing are both zero based, but the first and last # items in a slice can be confusing. See this ASCII diagram: @@ -133,12 +89,12 @@ class AllItemsResource: # Slice position: 0 1 2 3 4 5 6 # Index position: 0 1 2 3 4 5 # - # So if we have a list req_items with 240 items: + # So if we have a list items with 240 items: # - # 1st set: req_items[0:100] would give items at indexes 0 to 99 - # 2nd set: req_items[100:200] would give items at indexes 100 to 199 - # 3rd set: req_items[200:300] would give items at indexes 200 to 239 - items_subset: list = req_items[first_item:last_item] + # 1st set: items[0:100] would give items at indexes 0 to 99 + # 2nd set: items[100:200] would give items at indexes 100 to 199 + # 3rd set: items[200:300] would give items at indexes 200 to 239 + items_subset: list = req.context.items[first_item:last_item] views: dict = get_views(solr_date_string, items_subset) downloads: dict = get_downloads(solr_date_string, items_subset) @@ -152,9 +108,9 @@ class AllItemsResource: statistics.append({"id": k, "views": v, "downloads": downloads[k]}) message = { - "currentPage": req_page, + "currentPage": req.context.page, "totalPages": pages, - "limit": req_limit, + "limit": req.context.limit, "statistics": statistics, } diff --git a/dspace_statistics_api/util.py b/dspace_statistics_api/util.py index 87f104b..7361475 100644 --- a/dspace_statistics_api/util.py +++ b/dspace_statistics_api/util.py @@ -1,3 +1,6 @@ +import falcon + + def get_statistics_shards(): """Enumerate the cores in Solr to determine if statistics have been sharded into yearly shards by DSpace's stats-util or not (for example: statistics-2018). @@ -55,7 +58,6 @@ def get_statistics_shards(): def is_valid_date(date): import datetime - import falcon try: # Solr date format is: 2020-01-01T00:00:00Z @@ -68,3 +70,68 @@ def is_valid_date(date): title="Invalid parameter", description=f"Invalid date format: {date}. The value must be in format: 2020-01-01T00:00:00Z.", ) + + +def validate_items_post_parameters(req, resp, resource, params): + """Check the POSTed request parameters for the `/items` endpoint. + + Meant to be used as a `before` hook. + """ + import json + + # Only attempt to read the POSTed request if its length is not 0 (or + # rather, in the Python sense, if length is not a False-y value). + if req.content_length: + doc = json.load(req.bounded_stream) + else: + raise falcon.HTTPBadRequest( + title="Invalid request", description=f"Request body is empty." + ) + + # Parse date parameters from request body (will raise an HTTPBadRequest + # from is_valid_date() if any parameters are invalid) + if "dateFrom" in doc and is_valid_date(doc["dateFrom"]): + req.context.dateFrom = doc["dateFrom"] + else: + req.context.dateFrom = None + + if "dateTo" in doc and is_valid_date(doc["dateTo"]): + req.context.dateTo = doc["dateTo"] + else: + req.context.dateTo = None + + # Parse the limit parameter from the POST request body + if "limit" in doc: + if isinstance(doc["limit"], int) and 0 < doc["limit"] < 100: + req.context.limit = doc["limit"] + else: + raise falcon.HTTPBadRequest( + title="Invalid parameter", + description=f'The "limit" parameter is invalid. The value must be an integer between 0 and 100.', + ) + else: + req.context.limit = 100 + + # Parse the page parameter from the POST request body + if "page" in doc: + if isinstance(doc["page"], int) and doc["page"] >= 0: + req.context.page = doc["page"] + else: + raise falcon.HTTPBadRequest( + title="Invalid parameter", + description=f'The "page" parameter is invalid. The value must be at least 0.', + ) + else: + req.context.page = 0 + + # Parse the list of items from the POST request body + if "items" in doc: + if isinstance(doc["items"], list) and len(doc["items"]) > 0: + req.context.items = doc["items"] + else: + raise falcon.HTTPBadRequest( + title="Invalid parameter", + description=f'The "items" parameter is invalid. The value must be a comma-separated list of item UUIDs.', + ) + else: + req.context.items = list()