Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

permissions: add self-checkout #1222

Merged
merged 2 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion invenio_app_ils/circulation/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@
PatronOwnerPermission,
authenticated_user_permission,
backoffice_permission,
loan_checkout_permission,
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import arrow
from flask import current_app
from invenio_circulation.records.loaders.schemas.json import DateString
from invenio_app_ils.proxies import current_app_ils
from marshmallow import (
Schema,
ValidationError,
Expand All @@ -22,6 +21,8 @@
validates_schema,
)

from invenio_app_ils.proxies import current_app_ils

from .base import LoanBaseSchemaV1


Expand Down
2 changes: 1 addition & 1 deletion invenio_app_ils/documents/jsonresolvers/document_stock.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import jsonresolver
from werkzeug.routing import Rule

from invenio_app_ils.proxies import current_app_ils
from invenio_app_ils.eitems.api import EItem
from invenio_app_ils.proxies import current_app_ils

# Note: there must be only one resolver per file,
# otherwise only the last one is registered
Expand Down
2 changes: 1 addition & 1 deletion invenio_app_ils/documents/loaders/jsonschemas/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
ChangedBySchema,
set_changed_by,
)
from invenio_app_ils.records.loaders.schemas.identifiers import IdentifierSchema
from invenio_app_ils.records.loaders.schemas.preserve_cover_metadata import (
preserve_cover_metadata,
)
from invenio_app_ils.records.loaders.schemas.identifiers import IdentifierSchema


class AffiliationSchema(Schema):
Expand Down
2 changes: 1 addition & 1 deletion invenio_app_ils/eitems/loaders/jsonschemas/eitems.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
from marshmallow import EXCLUDE, Schema, fields, pre_load, validate

from invenio_app_ils.eitems.api import EItem
from invenio_app_ils.records.loaders.schemas.identifiers import IdentifierSchema
from invenio_app_ils.records.loaders.schemas.changed_by import (
ChangedBySchema,
set_changed_by,
)
from invenio_app_ils.records.loaders.schemas.identifiers import IdentifierSchema


class URLSchema(Schema):
Expand Down
14 changes: 13 additions & 1 deletion invenio_app_ils/items/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,27 @@ def build_resolver_fields(cls, data):
)
}

@classmethod
def enforce_constraints(cls, data, **kwargs):
"""Enforce constraints.

:param data (dict): dict that can be mutated to enforce constraints.
"""
# barcode is a required field and it should be always uppercase
data["barcode"] = data["barcode"].upper()

@classmethod
def create(cls, data, id_=None, **kwargs):
"""Create Item record."""
cls.build_resolver_fields(data)
return super().create(data, id_=id_, **kwargs)
cls.enforce_constraints(data, **kwargs)
created = super().create(data, id_=id_, **kwargs)
return created

def update(self, *args, **kwargs):
"""Update Item record."""
super().update(*args, **kwargs)
self.enforce_constraints(self)
self.build_resolver_fields(self)

def delete(self, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion invenio_app_ils/items/loaders/jsonschemas/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
ChangedBySchema,
set_changed_by,
)
from invenio_app_ils.records.loaders.schemas.price import PriceSchema
from invenio_app_ils.records.loaders.schemas.identifiers import IdentifierSchema
from invenio_app_ils.records.loaders.schemas.price import PriceSchema


class ISBNSchema(Schema):
Expand Down
8 changes: 4 additions & 4 deletions invenio_app_ils/items/serializers/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def transform_record(self, pid, record, links_factory=None, **kwargs):
pid, record, links_factory=links_factory, **kwargs
)
filter_circulation(item)
field_cover_metadata(item.get('metadata', {}).get("document", {}))
field_cover_metadata(item.get("metadata", {}).get("document", {}))
return item

def transform_search_hit(self, pid, record_hit, links_factory=None, **kwargs):
Expand All @@ -58,7 +58,7 @@ def transform_search_hit(self, pid, record_hit, links_factory=None, **kwargs):
pid, record_hit, links_factory=links_factory, **kwargs
)
filter_circulation(hit)
field_cover_metadata(hit.get('metadata', {}).get("document", {}))
field_cover_metadata(hit.get("metadata", {}).get("document", {}))
return hit


Expand All @@ -71,7 +71,7 @@ def transform_record(self, pid, record, links_factory=None, **kwargs):
pid, record, links_factory=links_factory, **kwargs
)
filter_circulation(item)
field_cover_metadata(item.get('metadata', {}).get("document", {}))
field_cover_metadata(item.get("metadata", {}).get("document", {}))
return item

def transform_search_hit(self, pid, record_hit, links_factory=None, **kwargs):
Expand All @@ -80,5 +80,5 @@ def transform_search_hit(self, pid, record_hit, links_factory=None, **kwargs):
pid, record_hit, links_factory=links_factory, **kwargs
)
filter_circulation(hit)
field_cover_metadata(hit.get('metadata', {}).get("document", {}))
field_cover_metadata(hit.get("metadata", {}).get("document", {}))
return hit
78 changes: 43 additions & 35 deletions invenio_app_ils/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,25 +134,27 @@ def patron_owner_permission(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
# CLI or Celery task
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()

# ensure that only the loan's patron can do operations on this loan
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
):
loan = kwargs["record"]
is_patron_current_user = current_user.id == int(loan["patron_pid"])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should really avoid using .get when the dict field is required. We need to fail early in these cases.


if current_app.config["ILS_SELF_CHECKOUT_ENABLED"] and is_patron_current_user:
return authenticated_user_permission()
raise LoanCheckoutByPatronForbidden(int(loan.get("patron_pid")), current_user.id)

raise LoanCheckoutByPatronForbidden(int(loan["patron_pid"]), current_user.id)


class PatronOwnerPermission(Permission):
Expand All @@ -163,36 +165,42 @@ def __init__(self, record):
super().__init__(UserNeed(int(record["patron_pid"])), backoffice_access_action)


_is_authenticated_user = [
Copy link
Contributor Author

@ntarocco ntarocco Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved outside to declare them once, and not on each func call.

"circulation-loan-request",
"patron-loans",
"bulk-loan-extension",
]
_is_backoffice_permission = [
"circulation-loan-force-checkout",
"circulation-overdue-loan-notification",
"circulation-loan-update-dates",
"relations-create",
"relations-delete",
"stats-most-loaned",
"document-request-actions",
"bucket-create",
"ill-brwreq-patron-loan-create",
"ill-brwreq-patron-loan-extension-accept",
"ill-brwreq-patron-loan-extension-decline",
"send-notification-to-patron",
]
_is_patron_owner_permission = [
"document-request-decline",
"ill-brwreq-patron-loan-extension-request",
]


def views_permissions_factory(action):
"""Return ILS views permissions factory."""
is_authenticated_user = [
"circulation-loan-request",
"patron-loans",
"bulk-loan-extension",
]
is_backoffice_permission = [
"circulation-loan-checkout",
"circulation-loan-force-checkout",
"circulation-overdue-loan-notification",
"circulation-loan-update-dates",
"relations-create",
"relations-delete",
"stats-most-loaned",
"document-request-actions",
"bucket-create",
"ill-brwreq-patron-loan-create",
"ill-brwreq-patron-loan-extension-accept",
"ill-brwreq-patron-loan-extension-decline",
"send-notification-to-patron",
]
is_patron_owner_permission = [
"document-request-decline",
"ill-brwreq-patron-loan-extension-request",
]
if action in is_authenticated_user:
if action in _is_authenticated_user:
return authenticated_user_permission()
elif action in is_backoffice_permission:
elif action in _is_backoffice_permission:
return backoffice_permission()
elif action in is_patron_owner_permission:
elif action in _is_patron_owner_permission:
return PatronOwnerPermission
elif action == "circulation-loan-checkout":
if current_app.config["ILS_SELF_CHECKOUT_ENABLED"]:
return authenticated_user_permission()
else:
return backoffice_permission()
return deny_all()
2 changes: 1 addition & 1 deletion invenio_app_ils/records/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from invenio_app_ils.permissions import backoffice_permission
from invenio_app_ils.records.permissions import RecordPermission
from invenio_app_ils.series.api import SERIES_PID_TYPE
from invenio_app_ils.signals import record_viewed, file_downloaded
from invenio_app_ils.signals import file_downloaded, record_viewed


def create_document_stats_blueprint(app):
Expand Down
1 change: 1 addition & 0 deletions invenio_app_ils/records_relations/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ class IlsRecordWithRelations(IlsRecord):
def relations(self):
"""Get record relations."""
from .retriever import get_relations

return get_relations(self)

def clear(self):
Expand Down
7 changes: 6 additions & 1 deletion invenio_app_ils/relations/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
from invenio_app_ils.errors import RecordRelationsError

ILS_RELATION_TYPE = namedtuple(
"IlsRelationType", RelationType._fields + ("relation_class", "sort_by",)
"IlsRelationType",
RelationType._fields
+ (
"relation_class",
"sort_by",
),
)

LANGUAGE_RELATION = ILS_RELATION_TYPE(
Expand Down
2 changes: 1 addition & 1 deletion invenio_app_ils/series/loaders/jsonschemas/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
ChangedBySchema,
set_changed_by,
)
from invenio_app_ils.records.loaders.schemas.identifiers import IdentifierSchema
from invenio_app_ils.records.loaders.schemas.preserve_cover_metadata import (
preserve_cover_metadata,
)
from invenio_app_ils.records.loaders.schemas.identifiers import IdentifierSchema
from invenio_app_ils.series.api import Series


Expand Down
50 changes: 46 additions & 4 deletions tests/api/ils/items/test_items_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,32 @@

import pytest

from invenio_app_ils.errors import DocumentNotFoundError, InternalLocationNotFoundError
from invenio_app_ils.items.api import Item
from invenio_app_ils.circulation.search import get_active_loan_by_item_pid
from invenio_app_ils.errors import (
DocumentNotFoundError,
InternalLocationNotFoundError,
ItemHasPastLoansError,
)
from invenio_app_ils.items.api import ITEM_PID_TYPE, Item


def test_item_refs(app, testdata):
def test_item_create(app, testdata):
"""Test creation of an item."""
item = Item.create(
dict(
pid="itemid-99",
document_pid="docid-1",
internal_location_pid="ilocid-4",
created_by=dict(type="script", value="demo"),
barcode="348048",
barcode="cm-348048",
status="CAN_CIRCULATE",
circulation_restriction="NO_RESTRICTION",
medium="PAPER",
)
)
assert "$schema" in item
assert "document" in item and "$ref" in item["document"]
assert item["barcode"] == "CM-348048"
assert "internal_location" in item and "$ref" in item["internal_location"]

item = Item.get_record_by_pid("itemid-1")
Expand All @@ -39,6 +45,16 @@ def test_item_refs(app, testdata):
assert "internal_location" in item and item["internal_location"]["name"]


def test_item_update(app, db, testdata):
"""Test update of an item."""
item = Item.get_record_by_pid("itemid-1")
item.update(dict(barcode="cm-348048"))
item.commit()
db.session.commit()

assert Item.get_record_by_pid("itemid-1")["barcode"] == "CM-348048"


def test_item_validation(db, testdata):
"""Test validation when updating an item."""
item_pid = testdata["items"][0]["pid"]
Expand All @@ -55,3 +71,29 @@ def test_item_validation(db, testdata):
item["internal_location_pid"] = "not_found_pid"
with pytest.raises(InternalLocationNotFoundError):
item.commit()


def test_item_references(db, testdata):
"""Test references when updating an item."""

def get_active_loan_pid_and_item_pid():
for t in testdata["items"]:
if t["status"] == "CAN_CIRCULATE":
item_pid = dict(type=ITEM_PID_TYPE, value=t["pid"])
active_loan = get_active_loan_by_item_pid(item_pid).execute().hits
total = active_loan.total.value
if total > 0:
return t["pid"], active_loan[0]["pid"]

# change document pid while is on loan
item_pid, loan_pid = get_active_loan_pid_and_item_pid()
item = Item.get_record_by_pid(item_pid)
item["document_pid"] = "docid-1"
with pytest.raises(ItemHasPastLoansError):
item.commit()

# change document to one that does not exist
item = Item.get_record_by_pid("itemid-1")
item["document_pid"] = "not_found_doc"
with pytest.raises(DocumentNotFoundError):
item.commit()
Loading