diff --git a/rero_ils/modules/loans/api.py b/rero_ils/modules/loans/api.py index 72344afd45..c407a0c056 100644 --- a/rero_ils/modules/loans/api.py +++ b/rero_ils/modules/loans/api.py @@ -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. diff --git a/rero_ils/modules/patron_types/api.py b/rero_ils/modules/patron_types/api.py index b7c0a3d769..d8f8487db3 100644 --- a/rero_ils/modules/patron_types/api.py +++ b/rero_ils/modules/patron_types/api.py @@ -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. @@ -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 diff --git a/rero_ils/modules/patrons/api.py b/rero_ils/modules/patrons/api.py index 5544a1c336..986ffcff9b 100644 --- a/rero_ils/modules/patrons/api.py +++ b/rero_ils/modules/patrons/api.py @@ -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 @@ -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 @@ -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.""" diff --git a/rero_ils/modules/patrons/views.py b/rero_ils/modules/patrons/views.py index 99e587763a..cd8736813d 100644 --- a/rero_ils/modules/patrons/views.py +++ b/rero_ils/modules/patrons/views.py @@ -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 @@ -105,6 +105,19 @@ def number_of_patrons(): return jsonify(response) +@api_blueprint.route('//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__, diff --git a/tests/api/loans/test_loans_limits.py b/tests/api/loans/test_loans_limits.py index 0e60288f2b..be6f751fcd 100644 --- a/tests/api/loans/test_loans_limits.py +++ b/tests/api/loans/test_loans_limits.py @@ -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 @@ -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 diff --git a/tests/api/patrons/test_patrons_blocked.py b/tests/api/patrons/test_patrons_blocked.py index 342c13288e..8f6bd56e92 100644 --- a/tests/api/patrons/test_patrons_blocked.py +++ b/tests/api/patrons/test_patrons_blocked.py @@ -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, diff --git a/tests/api/patrons/test_patrons_rest.py b/tests/api/patrons/test_patrons_rest.py index f0af1ddbb9..83d7382770 100644 --- a/tests/api/patrons/test_patrons_rest.py +++ b/tests/api/patrons/test_patrons_rest.py @@ -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