From 1eb37bc0d7a58cd74f74bd36ca9f9ca33c0da390 Mon Sep 17 00:00:00 2001 From: Saksham Arora Date: Mon, 3 Jun 2024 16:00:04 +0200 Subject: [PATCH 1/2] circulation: Support self checkout by patrons --- invenio_app_ils/circulation/config.py | 3 +- invenio_app_ils/config.py | 4 + invenio_app_ils/errors.py | 14 ++++ invenio_app_ils/permissions.py | 26 ++++++- tests/api/circulation/test_loan_checkout.py | 83 ++++++++++++++++++++- 5 files changed, 127 insertions(+), 3 deletions(-) diff --git a/invenio_app_ils/circulation/config.py b/invenio_app_ils/circulation/config.py index ae72dac37..5e796e4e4 100644 --- a/invenio_app_ils/circulation/config.py +++ b/invenio_app_ils/circulation/config.py @@ -47,6 +47,7 @@ loan_extend_circulation_permission, patron_owner_permission, superuser_permission, + loan_checkout_permission, ) from .api import ILS_CIRCULATION_LOAN_FETCHER, ILS_CIRCULATION_LOAN_MINTER @@ -161,7 +162,7 @@ dest="ITEM_ON_LOAN", trigger="checkout", transition=ILSToItemOnLoan, - permission_factory=backoffice_permission, + permission_factory=loan_checkout_permission, ), ], "PENDING": [ diff --git a/invenio_app_ils/config.py b/invenio_app_ils/config.py index 87ff77524..41342923a 100644 --- a/invenio_app_ils/config.py +++ b/invenio_app_ils/config.py @@ -865,6 +865,7 @@ def _(x): # ILS # === + ILS_VIEWS_PERMISSIONS_FACTORY = views_permissions_factory """Permissions factory for ILS views to handle all ILS actions.""" @@ -1071,3 +1072,6 @@ def _(x): ILS_PATRON_SYSTEM_AGENT_CLASS = SystemAgent DB_VERSIONING_USER_MODEL = None + +# Feature Toggles +ILS_SELF_CHECKOUT_ENABLED = False diff --git a/invenio_app_ils/errors.py b/invenio_app_ils/errors.py index 51dbef7b2..63bece127 100644 --- a/invenio_app_ils/errors.py +++ b/invenio_app_ils/errors.py @@ -180,6 +180,20 @@ def __init__(self, patron_pid, document_pid, **kwargs): ) +class LoanCheckoutByPatronForbidden(IlsException): + """A patron cannot checkout an item for another patron.""" + + code = 403 + description = "Forbidden. Patron '{current_user_pid}' cannot checkout item for another Patron '{patron_pid}'." + + def __init__(self, patron_pid, current_user_pid, **kwargs): + """Initialize LoanCheckoutByPatronForbidden exception.""" + super().__init__(**kwargs) + self.description = self.description.format( + patron_pid=patron_pid, current_user_pid=current_user_pid + ) + + class NotImplementedConfigurationError(IlsException): """Exception raised when function is not implemented.""" diff --git a/invenio_app_ils/permissions.py b/invenio_app_ils/permissions.py index b1e81adde..db2bc9f39 100644 --- a/invenio_app_ils/permissions.py +++ b/invenio_app_ils/permissions.py @@ -16,7 +16,7 @@ from invenio_access.permissions import Permission, authenticated_user, superuser_access from invenio_records_rest.utils import allow_all, deny_all -from invenio_app_ils.errors import InvalidLoanExtendError +from invenio_app_ils.errors import InvalidLoanExtendError, LoanCheckoutByPatronForbidden from invenio_app_ils.proxies import current_app_ils backoffice_access_action = action_factory("ils-backoffice-access") @@ -131,6 +131,30 @@ def patron_owner_permission(record): return PatronOwnerPermission(record) +def loan_checkout_permission(*args, **kwargs): + """Return permission to allow admins and librarians to checkout and patrons to self-checkout if enabled.""" + if not has_request_context(): + # If from CLI, don't allow self-checkout + return backoffice_permission() + if current_user.is_anonymous: + abort(401) + + is_admin_or_librarian = backoffice_permission().allows(g.identity) + if is_admin_or_librarian: + return backoffice_permission() + if len(args): + loan = args[0] + else: + loan = kwargs.get("record", {}) + is_patron_current_user = current_user.id == int(loan.get("patron_pid")) + if ( + current_app.config.get("ILS_SELF_CHECKOUT_ENABLED", False) + and is_patron_current_user + ): + return authenticated_user_permission() + raise LoanCheckoutByPatronForbidden(int(loan.get("patron_pid")), current_user.id) + + class PatronOwnerPermission(Permission): """Return Permission to evaluate if the current user owns the record.""" diff --git a/tests/api/circulation/test_loan_checkout.py b/tests/api/circulation/test_loan_checkout.py index a892e54ab..e08f07912 100644 --- a/tests/api/circulation/test_loan_checkout.py +++ b/tests/api/circulation/test_loan_checkout.py @@ -17,7 +17,12 @@ from invenio_access.permissions import Permission from invenio_app_ils.items.api import Item -from tests.helpers import user_login +from invenio_app_ils.permissions import ( + authenticated_user_permission, + loan_checkout_permission, + views_permissions_factory, +) +from tests.helpers import user_login, user_logout NEW_LOAN = { "item_pid": "CHANGE ME IN EACH TEST", @@ -208,3 +213,79 @@ def test_checkout_loader_start_end_dates(app, client, json_headers, users, testd params["transaction_user_pid"] = str(librarian.id) res = client.post(url, headers=json_headers, data=json.dumps(params)) assert res.status_code == 400 + + +def _views_permissions_factory(action): + """Override ILS views permissions factory.""" + if action == "circulation-loan-checkout": + return authenticated_user_permission() + return views_permissions_factory(action) + + +def test_self_checkout(app, client, json_headers, users, testdata): + """Tests for self checkout feature.""" + app.config["ILS_SELF_CHECKOUT_ENABLED"] = True + app.config["ILS_VIEWS_PERMISSIONS_FACTORY"] = _views_permissions_factory + app.config["RECORDS_REST_ENDPOINTS"]["pitmid"][ + "list_permission_factory_imp" + ] = authenticated_user_permission + app.config["ILS_CIRCULATION_RECORDS_REST_ENDPOINTS"]["loanid"][ + "update_permission_factory_imp" + ] = loan_checkout_permission + + # Self checkout by librarian should pass + librarian = users["librarian"] + user_login(client, "librarian", users) + params = deepcopy(NEW_LOAN) + params["item_pid"] = dict(type="pitmid", value="itemid-60") + params["transaction_user_pid"] = str(librarian.id) + params["patron_pid"] = str(librarian.id) + url = url_for("invenio_app_ils_circulation.loan_checkout") + res = client.post(url, headers=json_headers, data=json.dumps(params)) + + assert res.status_code == 202 + loan = res.get_json()["metadata"] + assert loan["state"] == "ITEM_ON_LOAN" + assert loan["item_pid"] == params["item_pid"] + assert loan["patron_pid"] == str(librarian.id) + user_logout(client) + + # Self checkout by patron should pass if patron_pid matches + patron3 = users["patron3"] + user_login(client, "patron3", users) + params = deepcopy(NEW_LOAN) + params["item_pid"] = dict(type="pitmid", value="itemid-61") + params["transaction_user_pid"] = str(patron3.id) + params["patron_pid"] = str(patron3.id) + url = url_for("invenio_app_ils_circulation.loan_checkout") + res = client.post(url, headers=json_headers, data=json.dumps(params)) + + assert res.status_code == 202 + loan = res.get_json()["metadata"] + assert loan["state"] == "ITEM_ON_LOAN" + assert loan["item_pid"] == params["item_pid"] + assert loan["patron_pid"] == str(patron3.id) + + # Self checkout should fail if feature flag is not set to true + app.config["ILS_SELF_CHECKOUT_ENABLED"] = False + params = deepcopy(NEW_LOAN) + params["item_pid"] = dict(type="pitmid", value="itemid-62") + params["transaction_user_pid"] = str(patron3.id) + params["patron_pid"] = str(patron3.id) + url = url_for("invenio_app_ils_circulation.loan_checkout") + res = client.post(url, headers=json_headers, data=json.dumps(params)) + + assert res.status_code == 403 + user_logout(client) + + # Self checkout should fail if if patron_pid doesn't match + patron1 = users["patron1"] + user_login(client, "patron1", users) + params = deepcopy(NEW_LOAN) + params["item_pid"] = dict(type="pitmid", value="itemid-63") + params["transaction_user_pid"] = str(patron1.id) + params["patron_pid"] = str(patron3.id) + url = url_for("invenio_app_ils_circulation.loan_checkout") + res = client.post(url, headers=json_headers, data=json.dumps(params)) + + assert res.status_code == 403 From da991e7975b99411cbceca96eb35d2fe299c557e Mon Sep 17 00:00:00 2001 From: Saksham Arora Date: Fri, 7 Jun 2024 14:07:09 +0200 Subject: [PATCH 2/2] release: v3.0.0rc3 --- CHANGES.rst | 5 +++++ invenio_app_ils/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3ac7499ce..dd85a7bfe 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,11 @@ Changes ======= +Version 3.0.0rc3 (released 2024-06-07) + +- circulation: Support self checkout by patrons +- Dockerfile: update backend base image python3.6 -> inveniosoftware/almalinux:1 + Version 3.0.0rc2 (released 2024-05-28) - mappings: Add alternative_titles in brwReqs and AcqOrders diff --git a/invenio_app_ils/__init__.py b/invenio_app_ils/__init__.py index b8a53115e..65849aa72 100644 --- a/invenio_app_ils/__init__.py +++ b/invenio_app_ils/__init__.py @@ -7,6 +7,6 @@ """invenio-app-ils.""" -__version__ = "3.0.0rc2" +__version__ = "3.0.0rc3" __all__ = ("__version__",)