Skip to content

Commit

Permalink
holdings: allow request for holding
Browse files Browse the repository at this point in the history
Co-Authored-by: Valeria Granata <[email protected]>
  • Loading branch information
2 people authored and Aly Badr committed Dec 1, 2021
1 parent 6415597 commit ce853ab
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 21 deletions.
10 changes: 10 additions & 0 deletions rero_ils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
from .modules.documents.api import Document
from .modules.documents.permissions import DocumentPermission
from .modules.holdings.api import Holding
from .modules.holdings.models import HoldingCirculationAction
from .modules.holdings.permissions import HoldingPermission
from .modules.ill_requests.api import ILLRequest
from .modules.ill_requests.permissions import ILLRequestPermission
Expand Down Expand Up @@ -2826,6 +2827,15 @@ def _(x):
]
}

HOLDING_CIRCULATION_ACTIONS_VALIDATION = {
HoldingCirculationAction.REQUEST: [
Location.can_request,
Holding.can_request,
CircPolicy.can_request,
Patron.can_request,
PatronType.can_request
]
}
# WIKI
# ====
WIKI_CONTENT_DIR = './wiki'
Expand Down
32 changes: 23 additions & 9 deletions rero_ils/modules/circ_policies/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,14 +373,17 @@ def get_reminder(self, reminder_type=DUE_SOON_REMINDER_TYPE, idx=0):
return reminders[idx]

@classmethod
def can_request(cls, item, **kwargs):
"""Check if the cipo corresponding to item/patron allow request.
def can_request(cls, record, **kwargs):
"""Check if the cipo corresponding to record/patron allow request.
:param item : the item to check
:param record : the record to check (Item or Holding)
:param kwargs : To be relevant, additional arguments should contains
'patron' argument.
:return a tuple with True|False and reasons to disallow if False.
"""
from ..holdings.api import Holding
from ..items.api import Item

patron = get_patron_from_arguments(**kwargs)
if not patron:
# none patron get be load from kwargs argument. This check can't
Expand All @@ -391,13 +394,24 @@ def can_request(cls, item, **kwargs):
# can't find any corresponding cipo --> return False
return False, ["Patron doesn't have the correct role"]
library_pid = kwargs['library'].pid if kwargs.get('library') \
else item.library_pid
else record.library_pid

if isinstance(record, Item):
record_circulation_category_pid = \
record.item_type_circulation_category_pid
elif isinstance(record, Holding):
record_circulation_category_pid = \
record.circulation_category_pid
else:
raise Exception(f'This resource cannot be \
requested : {record.__class__.__name__}')

cipo = cls.provide_circ_policy(
item.organisation_pid,
library_pid,
patron.patron_type_pid,
item.item_type_circulation_category_pid
)
record.organisation_pid,
library_pid,
patron.patron_type_pid,
record_circulation_category_pid
)
if not cipo.get('allow_requests', False):
return False, ["Circulation policy disallows the operation."]
return True, []
Expand Down
36 changes: 36 additions & 0 deletions rero_ils/modules/holdings/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from elasticsearch_dsl import Q
from flask import current_app
from flask_babelex import gettext as _
from invenio_records_rest.utils import obj_or_import_string
from jinja2 import Environment

from rero_ils.modules.items.models import ItemIssueStatus
Expand Down Expand Up @@ -445,6 +446,41 @@ def next_issue_display_text(self):
if self.patterns:
return self._get_next_issue_display_text(self.patterns)[0]

def get_location(self):
"""Shortcut to the location of holding."""
return Location.get_record_by_pid(self.location_pid)

def can(self, action, **kwargs):
"""Check if a specific action is allowed on this holding.
:param action : the action to check as HoldingCirculationAction part
:param kwargs : all others named arguments useful to check
:return a tuple with True|False to know if the action is possible and
a list of reasons to disallow if False.
"""
can, reasons = True, []
actions = current_app.config\
.get('HOLDING_CIRCULATION_ACTIONS_VALIDATION', {})
for func_name in actions['request']:
class_name = func_name.__self__.__name__
func_callback = obj_or_import_string(func_name)
func_can, func_reasons = func_callback(self, **kwargs)
reasons += func_reasons
can = can and func_can
return can, reasons

@classmethod
def can_request(cls, holding, **kwargs):
"""Check if holding can be requested depending on type.
:param holding : the holding to check
:param kwargs : addition arguments
:return a tuple with True|False and reasons to disallow if False.
"""
if holding and not holding.is_serial:
return False, [_('Only serial holdings can be requested.')]
return True, []

@classmethod
def _get_next_issue_display_text(cls, patterns):
"""Display the text for the next predicted issue.
Expand Down
49 changes: 49 additions & 0 deletions rero_ils/modules/holdings/api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@
from werkzeug.exceptions import NotFound, Unauthorized

from rero_ils.modules.errors import NoCirculationActionIsPermitted
from rero_ils.modules.holdings.models import HoldingCirculationAction
from rero_ils.modules.items.api import Item
from rero_ils.modules.items.models import ItemStatus
from rero_ils.modules.items.views.api_views import \
check_authentication_for_request, check_logged_user_authentication
from rero_ils.modules.libraries.api import Library
from rero_ils.modules.patrons.api import Patron
from rero_ils.modules.utils import get_ref_for_pid
from rero_ils.modules.views import check_authentication
Expand Down Expand Up @@ -245,3 +247,50 @@ def librarian_request(holding, item, data):
transaction_user_pid
"""
return item.request(**data)


@api_blueprint.route('/<holding_pid>/can_request', methods=['GET'])
@check_logged_user_authentication
@jsonify_error
def can_request(holding_pid):
"""HTTP request to check if an holding can be requested.
Depending of query string argument, check if either configuration
allows the request of the holding or if a librarian can request an
holding for a patron.
`api/holding/<holding_pid>/can_request` :
--> only check config
`api/holding/<holding_pid>/can_request?library_pid=<library_pid>&patron_barcode=<barcode>`:
--> check if the patron can request an holding (check the cipo)
"""
kwargs = {}

holding = Holding.get_record_by_pid(holding_pid)
if not holding:
abort(404, 'Holding not found')

patron_barcode = flask_request.args.get('patron_barcode')
if patron_barcode:
kwargs['patron'] = Patron.get_patron_by_barcode(
patron_barcode, holding.organisation_pid)
if not kwargs['patron']:
abort(404, 'Patron not found')

library_pid = flask_request.args.get('library_pid')
if library_pid:
kwargs['library'] = Library.get_record_by_pid(library_pid)
if not kwargs['library']:
abort(404, 'Library not found')

can, reasons = holding.can(HoldingCirculationAction.REQUEST, **kwargs)

# check the `reasons_not_request` array. If it's empty, the request is
# allowed, otherwise the request is not allowed and we need to return the
# reasons why
response = {'can': can}
if reasons:
response['reasons'] = {
'others': {reason: True for reason in reasons}
}
return jsonify(response)
6 changes: 6 additions & 0 deletions rero_ils/modules/holdings/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,9 @@ class HoldingNoteTypes:
# to display them on the interface
PUBLIC = [
]


class HoldingCirculationAction:
"""Enum class to list all possible action about an holding."""

REQUEST = 'request'
10 changes: 5 additions & 5 deletions rero_ils/modules/locations/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,15 @@ def restrict_pickup_to(self):
return location_pids

@classmethod
def can_request(cls, item, **kwargs):
"""Check if an item can be requested regarding its location.
def can_request(cls, record, **kwargs):
"""Check if an record can be requested regarding its location.
:param item : the item to check
:param record : the record to check
:param kwargs : addition arguments
:return a tuple with True|False and reasons to disallow if False.
"""
if item and not item.get_location().get('allow_request', False):
return False, [_('Item location disallows request.')]
if record and not record.get_location().get('allow_request', False):
return False, [_('Record location disallows request.')]
return True, []

def transaction_location_validator(self, location_pid):
Expand Down
7 changes: 0 additions & 7 deletions rero_ils/modules/patrons/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,6 @@ def _get_user_by_user_id(cls, user_id):
"""
return _datastore.find_user(id=user_id)

@property
def profile_url(self):
"""Get the link to the RERO_ILS patron profile URL."""
view_code = self.get_organisation().get('code')
base_url = current_app.config.get('RERO_ILS_APP_URL')
return f'{base_url}/{view_code}/patrons/profile'

# TODO: use cached property one we found how to invalidate the cache when
# the user change
@property
Expand Down
65 changes: 65 additions & 0 deletions tests/api/holdings/test_holdings_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,68 @@ def test_holdings_post_put_delete(client, holding_lib_martigny_data_tmp,

res = client.get(item_url)
assert res.status_code == 410


def test_holding_request(client, librarian_martigny, patron_martigny,
holding_lib_martigny, holding_lib_martigny_w_patterns,
lib_martigny, circ_policy_short_martigny):
"""Test holding can be requested"""
# test patron can request holding
login_user_via_session(client, patron_martigny.user)
patron = patron_martigny

res = client.get(
url_for(
'api_holding.can_request',
holding_pid=holding_lib_martigny_w_patterns.pid,
library_pid=lib_martigny.pid,
patron_barcode=patron.patron.get('barcode')
)
)
response = json.loads(res.data)
assert response['can']

# test librarian can request holding
login_user_via_session(client, librarian_martigny.user)
patron = librarian_martigny

res = client.get(
url_for(
'api_holding.can_request',
holding_pid=holding_lib_martigny_w_patterns.pid,
library_pid=lib_martigny.pid,
patron_barcode=patron.patron.get('barcode')
)
)
response = json.loads(res.data)
assert response['can']

# test patron cannot request holding
login_user_via_session(client, patron_martigny.user)
patron = patron_martigny

res = client.get(
url_for(
'api_holding.can_request',
holding_pid=holding_lib_martigny.pid,
library_pid=lib_martigny.pid,
patron_barcode=patron.patron.get('barcode')
)
)
response = json.loads(res.data)
assert not response['can']

# test librarian cannot request holding
login_user_via_session(client, librarian_martigny.user)
patron = librarian_martigny

res = client.get(
url_for(
'api_holding.can_request',
holding_pid=holding_lib_martigny.pid,
library_pid=lib_martigny.pid,
patron_barcode=patron.patron.get('barcode')
)
)
response = json.loads(res.data)
assert not response['can']

0 comments on commit ce853ab

Please sign in to comment.