Skip to content

Commit

Permalink
circulation: Support self checkout by patrons
Browse files Browse the repository at this point in the history
  • Loading branch information
sakshamarora1 committed Jun 7, 2024
1 parent 36d0212 commit 89635a3
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 3 deletions.
3 changes: 2 additions & 1 deletion invenio_app_ils/circulation/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -161,7 +162,7 @@
dest="ITEM_ON_LOAN",
trigger="checkout",
transition=ILSToItemOnLoan,
permission_factory=backoffice_permission,
permission_factory=loan_checkout_permission,
),
],
"PENDING": [
Expand Down
4 changes: 4 additions & 0 deletions invenio_app_ils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,7 @@ def _(x):

# ILS
# ===

ILS_VIEWS_PERMISSIONS_FACTORY = views_permissions_factory
"""Permissions factory for ILS views to handle all ILS actions."""

Expand Down Expand Up @@ -1071,3 +1072,6 @@ def _(x):
ILS_PATRON_SYSTEM_AGENT_CLASS = SystemAgent

DB_VERSIONING_USER_MODEL = None

# Feature Toggles
ILS_SELF_CHECKOUT_ENABLED = False
14 changes: 14 additions & 0 deletions invenio_app_ils/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
26 changes: 25 additions & 1 deletion invenio_app_ils/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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."""

Expand Down
75 changes: 74 additions & 1 deletion tests/api/circulation/test_loan_checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
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,
)
from tests.helpers import user_login, user_logout

NEW_LOAN = {
"item_pid": "CHANGE ME IN EACH TEST",
Expand Down Expand Up @@ -208,3 +212,72 @@ 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 test_self_checkout(app, client, json_headers, users, testdata):
"""Tests for self checkout feature."""
app.config["ILS_SELF_CHECKOUT_ENABLED"] = True
app.config["ILS_AUTHENTICATED_USER_PERMISSIONS"].append("circulation-loan-checkout")

Check failure on line 220 in tests/api/circulation/test_loan_checkout.py

View workflow job for this annotation

GitHub Actions / Tests (circulation, 3.9, pypi, postgresql13, opensearch2)

test_self_checkout KeyError: 'ILS_AUTHENTICATED_USER_PERMISSIONS'

Check failure on line 220 in tests/api/circulation/test_loan_checkout.py

View workflow job for this annotation

GitHub Actions / Tests (circulation, 3.9, pypi, postgresql14, opensearch2)

test_self_checkout KeyError: 'ILS_AUTHENTICATED_USER_PERMISSIONS'
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

0 comments on commit 89635a3

Please sign in to comment.