2021-03-22 12:42:42 +01:00
|
|
|
# SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
|
2020-12-18 21:42:06 +01:00
|
|
|
import datetime
|
|
|
|
import json
|
|
|
|
import re
|
|
|
|
|
2020-09-26 17:37:14 +02:00
|
|
|
import falcon
|
2020-12-18 21:42:06 +01:00
|
|
|
import requests
|
|
|
|
|
|
|
|
from .config import SOLR_SERVER
|
2020-09-26 17:37:14 +02:00
|
|
|
|
|
|
|
|
2020-09-24 11:03:12 +02:00
|
|
|
def get_statistics_shards():
|
2020-09-24 11:06:27 +02:00
|
|
|
"""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).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
str:A list of Solr statistics shards separated by commas.
|
|
|
|
"""
|
2020-10-06 14:12:13 +02:00
|
|
|
|
2020-09-24 11:03:12 +02:00
|
|
|
# Initialize an empty list for statistics core years
|
|
|
|
statistics_core_years = []
|
|
|
|
|
|
|
|
# URL for Solr status to check active cores
|
|
|
|
solr_query_params = {"action": "STATUS", "wt": "json"}
|
|
|
|
solr_url = SOLR_SERVER + "/admin/cores"
|
|
|
|
res = requests.get(solr_url, params=solr_query_params)
|
|
|
|
|
|
|
|
if res.status_code == requests.codes.ok:
|
|
|
|
data = res.json()
|
|
|
|
|
|
|
|
# Iterate over active cores from Solr's STATUS response (cores are in
|
|
|
|
# the status array of this response).
|
|
|
|
for core in data["status"]:
|
|
|
|
# Pattern to match, for example: statistics-2018
|
|
|
|
pattern = re.compile("^statistics-[0-9]{4}$")
|
|
|
|
|
|
|
|
if not pattern.match(core):
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Append current core to list
|
|
|
|
statistics_core_years.append(core)
|
|
|
|
|
|
|
|
# Initialize a string to hold our shards (may end up being empty if the Solr
|
|
|
|
# core has not been processed by stats-util).
|
|
|
|
shards = str()
|
|
|
|
|
|
|
|
if len(statistics_core_years) > 0:
|
|
|
|
# Begin building a string of shards starting with the default one
|
|
|
|
shards = f"{SOLR_SERVER}/statistics"
|
|
|
|
|
|
|
|
for core in statistics_core_years:
|
|
|
|
# Create a comma-separated list of shards to pass to our Solr query
|
|
|
|
#
|
|
|
|
# See: https://wiki.apache.org/solr/DistributedSearch
|
|
|
|
shards += f",{SOLR_SERVER}/{core}"
|
|
|
|
|
|
|
|
# Return the string of shards, which may actually be empty. Solr doesn't
|
|
|
|
# seem to mind if the shards query parameter is empty and I haven't seen
|
|
|
|
# any negative performance impact so this should be fine.
|
|
|
|
return shards
|
dspace_statistics_api: Add support for date ranges to /items
You can now POST a JSON request to /items with a list of items and
a date range. This allows the possibility to get view and download
statistics for arbitrary items and arbitrary date ranges.
The JSON request should be in the following format:
{
"limit": 100,
"page": 0,
"dateFrom": "2020-01-01T00:00:00Z",
"dateTo": "2020-09-09T00:00:00Z",
"items": [
"f44cf173-2344-4eb2-8f00-ee55df32c76f",
"2324aa41-e9de-4a2b-bc36-16241464683e",
"8542f9da-9ce1-4614-abf4-f2e3fdb4b305",
"0fe573e7-042a-4240-a4d9-753b61233908"
]
}
The limit, page, and date parameters are all optional. By default
it will use a limit of 100, page 0, and [* TO *] Solr date range.
2020-09-25 11:21:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
def is_valid_date(date):
|
|
|
|
try:
|
|
|
|
# Solr date format is: 2020-01-01T00:00:00Z
|
|
|
|
# See: https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
|
|
|
|
datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
|
|
|
return True
|
|
|
|
except ValueError:
|
|
|
|
raise falcon.HTTPBadRequest(
|
|
|
|
title="Invalid parameter",
|
|
|
|
description=f"Invalid date format: {date}. The value must be in format: 2020-01-01T00:00:00Z.",
|
|
|
|
)
|
2020-09-26 17:37:14 +02:00
|
|
|
|
|
|
|
|
Add communities and collections support to API
The basic logic is similar to items, where you can request single
item statistics with a UUID, all item statistics, and item statis-
tics for a list of items (optionally with a date range). Most of
the item code was re-purposed to work on "elements", which can be
items, communities, or collections depending on the request, with
the use of Falcon's `before` hooks to set the statistics scope so
we know how to behave for the current request.
Other than the minor difference in facet fields, another issue I
had with communities and collections is that the owningComm and
owningColl fields are multi-valued (unlike items' id field). This
means that, when you facet the results of your query, Solr returns
ids that seem unrelated, but are actually present in the field, so
I had to make sure I checked all returned ids to see if they were
in the user's POSTed elements list.
TODO:
- Add tests
- Revise docstrings
- Refactor items.py as it is now generic
2020-12-20 15:14:46 +01:00
|
|
|
def validate_post_parameters(req, resp, resource, params):
|
|
|
|
"""Check the POSTed request parameters for the `/items`, `/communities` and
|
|
|
|
`/collections` endpoints.
|
2020-09-26 17:37:14 +02:00
|
|
|
|
|
|
|
Meant to be used as a `before` hook.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# 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(
|
2020-10-06 14:11:12 +02:00
|
|
|
title="Invalid request", description="Request body is empty."
|
2020-09-26 17:37:14 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
# 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:
|
2020-11-18 20:55:54 +01:00
|
|
|
if isinstance(doc["limit"], int) and 0 < doc["limit"] <= 100:
|
2020-09-26 17:37:14 +02:00
|
|
|
req.context.limit = doc["limit"]
|
|
|
|
else:
|
|
|
|
raise falcon.HTTPBadRequest(
|
|
|
|
title="Invalid parameter",
|
2020-11-02 20:59:20 +01:00
|
|
|
description='The "limit" parameter is invalid. The value must be an integer between 1 and 100.',
|
2020-09-26 17:37:14 +02:00
|
|
|
)
|
|
|
|
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",
|
2020-10-06 14:11:12 +02:00
|
|
|
description='The "page" parameter is invalid. The value must be at least 0.',
|
2020-09-26 17:37:14 +02:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
req.context.page = 0
|
|
|
|
|
Add communities and collections support to API
The basic logic is similar to items, where you can request single
item statistics with a UUID, all item statistics, and item statis-
tics for a list of items (optionally with a date range). Most of
the item code was re-purposed to work on "elements", which can be
items, communities, or collections depending on the request, with
the use of Falcon's `before` hooks to set the statistics scope so
we know how to behave for the current request.
Other than the minor difference in facet fields, another issue I
had with communities and collections is that the owningComm and
owningColl fields are multi-valued (unlike items' id field). This
means that, when you facet the results of your query, Solr returns
ids that seem unrelated, but are actually present in the field, so
I had to make sure I checked all returned ids to see if they were
in the user's POSTed elements list.
TODO:
- Add tests
- Revise docstrings
- Refactor items.py as it is now generic
2020-12-20 15:14:46 +01:00
|
|
|
# Parse the list of elements from the POST request body
|
|
|
|
if req.context.statistics_scope in doc:
|
|
|
|
if (
|
|
|
|
isinstance(doc[req.context.statistics_scope], list)
|
|
|
|
and len(doc[req.context.statistics_scope]) > 0
|
|
|
|
):
|
|
|
|
req.context.elements = doc[req.context.statistics_scope]
|
2020-09-26 17:37:14 +02:00
|
|
|
else:
|
|
|
|
raise falcon.HTTPBadRequest(
|
|
|
|
title="Invalid parameter",
|
Add communities and collections support to API
The basic logic is similar to items, where you can request single
item statistics with a UUID, all item statistics, and item statis-
tics for a list of items (optionally with a date range). Most of
the item code was re-purposed to work on "elements", which can be
items, communities, or collections depending on the request, with
the use of Falcon's `before` hooks to set the statistics scope so
we know how to behave for the current request.
Other than the minor difference in facet fields, another issue I
had with communities and collections is that the owningComm and
owningColl fields are multi-valued (unlike items' id field). This
means that, when you facet the results of your query, Solr returns
ids that seem unrelated, but are actually present in the field, so
I had to make sure I checked all returned ids to see if they were
in the user's POSTed elements list.
TODO:
- Add tests
- Revise docstrings
- Refactor items.py as it is now generic
2020-12-20 15:14:46 +01:00
|
|
|
description=f'The "{req.context.statistics_scope}" parameter is invalid. The value must be a comma-separated list of UUIDs.',
|
2020-09-26 17:37:14 +02:00
|
|
|
)
|
|
|
|
else:
|
2023-12-09 10:33:46 +01:00
|
|
|
req.context.elements = []
|
Add communities and collections support to API
The basic logic is similar to items, where you can request single
item statistics with a UUID, all item statistics, and item statis-
tics for a list of items (optionally with a date range). Most of
the item code was re-purposed to work on "elements", which can be
items, communities, or collections depending on the request, with
the use of Falcon's `before` hooks to set the statistics scope so
we know how to behave for the current request.
Other than the minor difference in facet fields, another issue I
had with communities and collections is that the owningComm and
owningColl fields are multi-valued (unlike items' id field). This
means that, when you facet the results of your query, Solr returns
ids that seem unrelated, but are actually present in the field, so
I had to make sure I checked all returned ids to see if they were
in the user's POSTed elements list.
TODO:
- Add tests
- Revise docstrings
- Refactor items.py as it is now generic
2020-12-20 15:14:46 +01:00
|
|
|
|
|
|
|
|
|
|
|
def set_statistics_scope(req, resp, resource, params):
|
|
|
|
"""Set the statistics scope (item, collection, or community) of the request
|
|
|
|
as well as the appropriate database (for GET requests) and Solr facet fields
|
|
|
|
(for POST requests).
|
|
|
|
|
|
|
|
Meant to be used as a `before` hook.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Extract the scope from the request path. This is *guaranteed* to be one
|
|
|
|
# of the following values because we only send requests matching these few
|
|
|
|
# patterns to routes using this set_statistics_scope hook.
|
|
|
|
#
|
|
|
|
# Note: this regex is ordered so that "items" and "collections" match before
|
|
|
|
# "item" and "collection".
|
|
|
|
req.context.statistics_scope = re.findall(
|
|
|
|
r"^/(communities|community|collections|collection|items|item)", req.path
|
|
|
|
)[0]
|
|
|
|
|
|
|
|
# Set the correct database based on the statistics_scope. The database is
|
|
|
|
# used for all GET requests where statistics are returned directly from the
|
|
|
|
# database. In this case we can return early.
|
|
|
|
if req.method == "GET":
|
|
|
|
if re.findall(r"^(item|items)$", req.context.statistics_scope):
|
|
|
|
req.context.database = "items"
|
|
|
|
elif re.findall(r"^(community|communities)$", req.context.statistics_scope):
|
|
|
|
req.context.database = "communities"
|
|
|
|
elif re.findall(r"^(collection|collections)$", req.context.statistics_scope):
|
|
|
|
req.context.database = "collections"
|
|
|
|
|
|
|
|
# GET requests only need the scope and the database so we can return now
|
|
|
|
return
|
|
|
|
|
|
|
|
# If the current request is for a plural items, communities, or collections
|
|
|
|
# that includes a list of element ids POSTed with the request body then we
|
|
|
|
# need to set the Solr facet field so we can get the live results.
|
|
|
|
if req.method == "POST":
|
|
|
|
if req.context.statistics_scope == "items":
|
|
|
|
req.context.views_facet_field = "id"
|
|
|
|
req.context.downloads_facet_field = "owningItem"
|
|
|
|
elif req.context.statistics_scope == "communities":
|
|
|
|
req.context.views_facet_field = "owningComm"
|
|
|
|
req.context.downloads_facet_field = "owningComm"
|
|
|
|
elif req.context.statistics_scope == "collections":
|
|
|
|
req.context.views_facet_field = "owningColl"
|
|
|
|
req.context.downloads_facet_field = "owningColl"
|
2020-12-20 15:31:52 +01:00
|
|
|
|
|
|
|
|
|
|
|
# vim: set sw=4 ts=4 expandtab:
|