Skip to content

Commit

Permalink
patron: add circulation informations API
Browse files Browse the repository at this point in the history
Adds an API to get patron circulation informations. This API return
the number of loans relate to this patron by state and messages useful
for the circulation about this patron (patron blocked, checkout limit,
...)

Closes rero#1278

Co-Authored-by: Renaud Michotte <renaud.michotte@gmail.com>
  • Loading branch information
zannkukai committed Oct 28, 2020
1 parent 9950b51 commit cb2b3d5
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 16 deletions.
17 changes: 17 additions & 0 deletions rero_ils/modules/loans/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,23 @@ def get_loans_by_item_pid_by_patron_pid(
return {}


def get_loans_stats_by_patron_pid(patron_pid):
"""Search loans for patron and aggregate result on loan state.
:param patron_pid: The patron pid
:return: a dict with loans state as key, number of loans as value
"""
agg = A('terms', field='state')
search = search_by_patron_item_or_document(patron_pid=patron_pid)
search.aggs.bucket('state', agg)
search = search[0:0]
results = search.execute()
stats = {}
for result in results.aggregations.state.buckets:
stats[result.key] = result.doc_count
return stats


def get_loans_by_patron_pid(patron_pid, filter_states=[]):
"""Search all loans for patron to the given filter_states.
Expand Down
30 changes: 16 additions & 14 deletions rero_ils/modules/patron_types/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,14 +220,14 @@ def check_overdue_items_limit(self, patron):
return limit > len(overdue_items)
return True

def check_checkout_count_limit(self, patron, item):
def check_checkout_count_limit(self, patron, item=None):
"""Check if a patron reached the checkout limits.
* check the global general limit (if exists).
* check the library exception limit (if exists).
* check the library default limit (if exists).
:param patron: the patron who tries to execute the checkout.
:param item: the item related to the loan.
:param item: the item related to the loan (optionnal).
:return a tuple of two values ::
- True|False : to know if the check is success or not.
- message(string) : the reason why the check fails.
Expand All @@ -248,18 +248,20 @@ def check_checkout_count_limit(self, patron, item):
return False, _('Checkout denied: the maximal checkout number '
'is reached.')

# [3] check library_limit
item_library_pid = item.library_pid
library_limit_value = checkout_limits.get('library_limit')
# try to find an exception rule for this library
for exception in checkout_limits.get('library_exceptions', []):
if exception['library']['pid'] == item_library_pid:
library_limit_value = exception['value']
break
if library_limit_value and item_library_pid in patron_library_stats:
if patron_library_stats[item_library_pid] >= library_limit_value:
return False, _('Checkout denied: the maximal checkout number '
'of items for this library is reached.')
# [3] check library_limit if item is not none
if item:
item_lib_pid = item.library_pid
library_limit_value = checkout_limits.get('library_limit')
# try to find an exception rule for this library
for exception in checkout_limits.get('library_exceptions', []):
if exception['library']['pid'] == item_lib_pid:
library_limit_value = exception['value']
break
if library_limit_value and item_lib_pid in patron_library_stats:
if patron_library_stats[item_lib_pid] >= library_limit_value:
return False, _('Checkout denied: the maximal checkout '
'number of items for this library is '
'reached.')

# [4] no problem detected, checkout is allowed
return True, None
Expand Down
35 changes: 35 additions & 0 deletions rero_ils/modules/patrons/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ def can_request(cls, item, **kwargs):
# a blocked patron can't request any item
if patron.is_blocked:
return False, [patron.blocked_message]

return True, []

@classmethod
Expand All @@ -426,6 +427,7 @@ def can_checkout(cls, item, **kwargs):
# a blocked patron can't request any item
if patron.is_blocked:
return False, [patron.blocked_message]

return True, []

@classmethod
Expand Down Expand Up @@ -686,6 +688,39 @@ def transaction_user_validator(self, user_pid):
"""
return Patron.record_pid_exists(user_pid)

def get_circulation_messages(self):
"""Return messages useful for circulation.
* check if the user is blocked ?
* check if the user reaches the maximum loans limit ?
:return an array of messages. Each message is a dictionary with a level
and a content. The level could be used to filters messages if
needed.
"""
from ..patron_types.api import PatronType

# if patron is blocked - error type message
# if patron is blocked, no need to return any other circulation
# messages !
if self.is_blocked:
return [{
'type': 'error',
'content': self.blocked_message
}]

messages = []
# check the patron type define limit
patron_type = PatronType.get_record_by_pid(self.patron_type_pid)
valid, message = patron_type.check_checkout_count_limit(self)
if not valid:
messages.append({
'type': 'error',
'content': message
})

return messages


class PatronsIndexer(IlsRecordsIndexer):
"""Holdings indexing class."""
Expand Down
15 changes: 14 additions & 1 deletion rero_ils/modules/patrons/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from ..items.api import Item
from ..items.utils import item_pid_to_object
from ..libraries.api import Library
from ..loans.api import Loan, patron_profile
from ..loans.api import Loan, get_loans_stats_by_patron_pid, patron_profile
from ..locations.api import Location
from ..utils import get_base_url
from ...permissions import login_and_librarian
Expand Down Expand Up @@ -105,6 +105,19 @@ def number_of_patrons():
return jsonify(response)


@api_blueprint.route('/<patron_pid>/circulation_informations', methods=['GET'])
@check_permission
def patron_circulation_informations(patron_pid):
"""Get the circulation statistics and info messages about a patron."""
patron = Patron.get_record_by_pid(patron_pid)
if not patron:
abort(404, 'Patron not found')
return jsonify({
'statistics': get_loans_stats_by_patron_pid(patron_pid),
'messages': patron.get_circulation_messages()
})


blueprint = Blueprint(
'patrons',
__name__,
Expand Down
14 changes: 13 additions & 1 deletion tests/api/loans/test_loans_limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
"""Loan Record limits."""
from copy import deepcopy

from flask import url_for
from invenio_accounts.testutils import login_user_via_session
from utils import postdata
from utils import get_json, postdata

from rero_ils.modules.loans.api import LoanAction
from rero_ils.modules.patron_types.api import PatronType
Expand Down Expand Up @@ -133,6 +134,17 @@ def test_loans_limits_checkout_library_limits(
assert res.status_code == 403
assert 'Checkout denied' in data['message']

# check the circulation information API
url = url_for(
'api_patrons.patron_circulation_informations',
patron_pid=patron.pid
)
res = client.get(url)
assert res.status_code == 200
data = get_json(res)
assert 'warning' == data['messages'][0]['type']
assert 'Checkout denied' in data['messages'][0]['content']

# reset fixtures
# --> checkin both loaned item
# --> reset patron_type to original value
Expand Down
4 changes: 4 additions & 0 deletions tests/api/patrons/test_patrons_blocked.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ def test_blocked_field_exists(
assert 'blocked' in data['metadata']['patron']
assert data['metadata']['patron']['blocked'] is True

assert patron3_martigny_blocked_no_email.is_blocked
note = patron3_martigny_blocked_no_email.patron.get('blocked_note')
assert note and note in patron3_martigny_blocked_no_email.blocked_message


def test_blocked_field_not_present(
client,
Expand Down
35 changes: 35 additions & 0 deletions tests/api/patrons/test_patrons_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,3 +552,38 @@ def test_patrons_count(client, patron_sion_no_email,
res = client.get(url)
assert res.status_code == 200
assert get_json(res) == dict(hits=dict(total=1))


def test_patrons_circulation_informations(
client, patron_sion_no_email, librarian_martigny_no_email,
patron3_martigny_blocked_no_email):
"""test patron circulation informations."""
url = url_for(
'api_patrons.patron_circulation_informations',
patron_pid=patron_sion_no_email.pid
)
res = client.get(url)
assert res.status_code == 401

login_user_via_session(client, librarian_martigny_no_email.user)
res = client.get(url)
assert res.status_code == 200
data = get_json(res)
assert len(data['messages']) == 0

url = url_for(
'api_patrons.patron_circulation_informations',
patron_pid=patron3_martigny_blocked_no_email.pid
)
res = client.get(url)
assert res.status_code == 200
data = get_json(res)
assert 'error' == data['messages'][0]['type']
assert 'This patron is currently blocked' in data['messages'][0]['content']

url = url_for(
'api_patrons.patron_circulation_informations',
patron_pid='dummy_pid'
)
res = client.get(url)
assert res.status_code == 404

0 comments on commit cb2b3d5

Please sign in to comment.