From a73cafa665e68a33a9a865ee18fbe158eaf320bb Mon Sep 17 00:00:00 2001 From: Peter Weber Date: Wed, 14 Apr 2021 14:23:43 +0200 Subject: [PATCH] notifications: enhance notifications * Adds aggregated notification dispatch. * Adds time stamp monitoring cli. * closes #1236. Co-Authored-by: Peter Weber --- poetry.lock | 44 ++- pyproject.toml | 1 + rero_ils/config.py | 32 ++ rero_ils/modules/libraries/api.py | 23 +- rero_ils/modules/loans/api.py | 9 +- rero_ils/modules/loans/cli.py | 88 ++++-- rero_ils/modules/loans/listener.py | 3 +- rero_ils/modules/monitoring.py | 16 +- rero_ils/modules/notifications/api.py | 136 +++----- rero_ils/modules/notifications/cli.py | 45 ++- rero_ils/modules/notifications/dispatcher.py | 294 ++++++++++++------ .../notifications/notification-v0.0.1.json | 4 + .../v7/notifications/notification-v0.0.1.json | 3 + rero_ils/modules/notifications/tasks.py | 46 +-- .../templates/email/_patron_address.txt | 2 +- .../templates/email/availability/eng.txt | 11 +- .../templates/email/availability/fre.txt | 10 +- .../templates/email/availability/ger.txt | 10 +- .../templates/email/availability/ita.txt | 10 +- .../templates/email/due_soon/eng.txt | 4 +- .../templates/email/due_soon/fre.txt | 4 +- .../templates/email/due_soon/ger.txt | 4 +- .../templates/email/due_soon/ita.txt | 4 +- .../email/others/location_notification.txt | 2 +- .../templates/email/overdue/eng.txt | 6 +- .../templates/email/overdue/fre.txt | 6 +- .../templates/email/overdue/ger.txt | 6 +- .../templates/email/overdue/ita.txt | 6 +- .../templates/email/recall/eng.txt | 4 +- .../templates/email/recall/fre.txt | 4 +- .../templates/email/recall/ger.txt | 4 +- .../templates/email/recall/ita.txt | 4 +- scripts/setup | 4 +- tests/api/circulation/test_borrow_limits.py | 4 +- tests/api/loans/test_loans_rest.py | 4 +- .../notifications/test_notifications_rest.py | 17 +- tests/api/selfcheck/test_selfcheck.py | 7 +- tests/api/test_tasks.py | 6 +- tests/data/data.json | 7 +- tests/fixtures/circulation.py | 66 ++++ .../notifications/test_notifications_api.py | 90 ++---- 41 files changed, 682 insertions(+), 368 deletions(-) diff --git a/poetry.lock b/poetry.lock index 41a7eff21b..2cb09a05f4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -442,6 +442,14 @@ all = ["Sphinx (>=2.4)", "pytest-cov (>=2.10.1)", "check-manifest (>=0.42)", "py docs = ["Sphinx (>=2.4)"] tests = ["pytest-cov (>=2.10.1)", "check-manifest (>=0.42)", "pytest-isort (>=1.2.0)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2.2.0)", "pytest (>=6,<7)"] +[[package]] +category = "main" +description = "Pythonic argument parser, that will make you smile" +name = "docopt" +optional = false +python-versions = "*" +version = "0.6.2" + [[package]] category = "main" description = "Docutils -- Python Documentation Utilities" @@ -893,6 +901,7 @@ WTForms = "*" reference = "647dd97880e2105948071b041286f318bf2628f7" type = "git" url = "https://github.com/rero/flask-wiki.git" + [[package]] category = "main" description = "Simple integration of Flask and WTForms." @@ -1045,7 +1054,7 @@ optional = true version = ">=1.2.5,<1.3.0" [package.dependencies.invenio-db] -extras = ["versioning", "postgresql"] +extras = ["postgresql", "versioning"] optional = true version = ">=1.0.8,<1.1.0" @@ -1319,6 +1328,7 @@ tests = ["mock (>=2.0.0)", "pytest-invenio (>=1.4.0,<1.5.0)", "pytest-mock (>=1. reference = "40e8d2fc3800319179ec84b26bb25ef8711e6356" type = "git" url = "https://github.com/inveniosoftware/invenio-circulation.git" + [[package]] category = "main" description = "Invenio configuration loader." @@ -1521,6 +1531,7 @@ tests = ["check-manifest (>=0.35)", "coverage (>=4.3.4)", "isort (4.2.2)", "mock reference = "fe55e095a8e78b36cfad875f752f2facc2908bae" type = "git" url = "https://github.com/inveniosoftware/invenio-oaiharvester.git" + [[package]] category = "main" description = "Invenio module that implements OAI-PMH server." @@ -1802,6 +1813,7 @@ tests = ["check-manifest (>=0.35)", "coverage (>=4.5.3)", "invenio-app (>=1.2.3) reference = "6087fcacb7ca93841005bdc6561df6de2b88362b" type = "git" url = "https://github.com/inveniosoftware-contrib/invenio-sip2.git" + [[package]] category = "main" description = "Invenio standard theme." @@ -1854,6 +1866,7 @@ tests = ["pytest-invenio (>=1.4.0)"] reference = "17bf3f045aef278bcf28aaf9a54b9faf3b03e715" type = "git" url = "https://github.com/rero/invenio-userprofiles.git" + [[package]] category = "main" description = "IPython: Productive Interactive Computing" @@ -2261,6 +2274,17 @@ setuptools = "*" [package.extras] testing = ["pytest"] +[[package]] +category = "main" +description = "Modules to convert numbers to words. Easily extensible." +name = "num2words" +optional = false +python-versions = "*" +version = "0.5.10" + +[package.dependencies] +docopt = ">=0.6.2" + [[package]] category = "main" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" @@ -3111,7 +3135,7 @@ test = ["pytest"] [[package]] category = "main" description = "Backported and Experimental Type Hints for Python 3.5+" -marker = "python_version < \"3.8\" or python_version >= \"3.7\" and python_version < \"3.8\"" +marker = "python_version < \"3.8\" or python_version >= \"3.7\" and python_version < \"3.8\" or python_version >= \"3.7\" and python_version < \"3.8\" and (python_version < \"3.8\" or python_version >= \"3.7\" and python_version < \"3.8\")" name = "typing-extensions" optional = false python-versions = "*" @@ -3325,7 +3349,7 @@ version = "0.12.0" [[package]] category = "main" description = "Backport of pathlib-compatible object wrapper for zip files" -marker = "python_version < \"3.8\" or python_version >= \"3.7\" and python_version < \"3.8\"" +marker = "python_version < \"3.8\" or python_version >= \"3.7\" and python_version < \"3.8\" or python_version >= \"3.7\" and python_version < \"3.8\" and (python_version < \"3.8\" or python_version >= \"3.7\" and python_version < \"3.8\")" name = "zipp" optional = false python-versions = ">=3.6" @@ -3339,7 +3363,8 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt sip2 = ["invenio-sip2"] [metadata] -content-hash = "6340dd9e780b2817c7d6503afee78ff07c37c7cf6c2fae89a4fb726c87dc230a" +content-hash = "688f8bc93d628d45ca1362a06adda8d418067c1f24725c704ffd30bd90965cfc" +lock-version = "1.0" python-versions = ">= 3.6, <3.8" [metadata.files] @@ -3484,7 +3509,6 @@ click-repl = [ ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, @@ -3498,6 +3522,9 @@ coverage = [ {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, + {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, + {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, + {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, @@ -3567,6 +3594,9 @@ docker-services-cli = [ {file = "Docker-Services-CLI-0.3.0.tar.gz", hash = "sha256:61e7ee909854c182290ec6806138044f09e39babc60cd9f3573b5ce6fb0d93f2"}, {file = "Docker_Services_CLI-0.3.0-py2.py3-none-any.whl", hash = "sha256:3f11e0b9f9734829ca448399c5a4820fe8ff765e851bab5f0957116440b898fe"}, ] +docopt = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] docutils = [ {file = "docutils-0.17-py2.py3-none-any.whl", hash = "sha256:a71042bb7207c03d5647f280427f14bfbd1a65c9eb84f4b341d85fafb6bb4bdf"}, {file = "docutils-0.17.tar.gz", hash = "sha256:e2ffeea817964356ba4470efba7c2f42b6b0de0b04e66378507e3e2504bbff4c"}, @@ -4029,6 +4059,10 @@ msgpack = [ node-semver = [ {file = "node-semver-0.1.1.tar.gz", hash = "sha256:e29ee4e51efb6d82c55aef5d569b888842e62e6404ce95df18d80c421f8e7dac"}, ] +num2words = [ + {file = "num2words-0.5.10-py3-none-any.whl", hash = "sha256:0b6e5f53f11d3005787e206d9c03382f459ef048a43c544e3db3b1e05a961548"}, + {file = "num2words-0.5.10.tar.gz", hash = "sha256:37cd4f60678f7e1045cdc3adf6acf93c8b41bf732da860f97d301f04e611cc57"}, +] oauthlib = [ {file = "oauthlib-2.1.0-py2.py3-none-any.whl", hash = "sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b"}, {file = "oauthlib-2.1.0.tar.gz", hash = "sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162"}, diff --git a/pyproject.toml b/pyproject.toml index 23dec737d2..c06b5f2b61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,7 @@ cryptography = ">3.3.1" freezegun = "^1.1.0" lazyreader = ">1.0.0" jinja2 = ">2.11.2" +num2words = "^0.5.10" [tool.poetry.dev-dependencies] ## Python packages development dependencies (order matters) diff --git a/rero_ils/config.py b/rero_ils/config.py index 5fe6ca580f..cf26444199 100644 --- a/rero_ils/config.py +++ b/rero_ils/config.py @@ -307,6 +307,38 @@ def _(x): 'enabled': False, # TODO: in production set this up once a day }, + 'notification-dispatch-due_soon': { + 'task': 'rero_ils.modules.notifications.tasks.process_notifications', + 'schedule': crontab(minute="*/15"), + 'kwargs': { + 'notification_type': Notification.DUE_SOON_NOTIFICATION_TYPE + }, + 'enabled': False, + }, + 'notification-dispatch-overdue': { + 'task': 'rero_ils.modules.notifications.tasks.process_notifications', + 'schedule': timedelta(minutes=15), + 'kwargs': { + 'notification_type': Notification.OVERDUE_NOTIFICATION_TYPE + }, + 'enabled': False, + }, + 'notification-dispatch-availability': { + 'task': 'rero_ils.modules.notifications.tasks.process_notifications', + 'schedule': timedelta(minutes=15), + 'kwargs': { + 'notification_type': Notification.AVAILABILITY_NOTIFICATION_TYPE + }, + 'enabled': False, + }, + 'notification-dispatch-recall': { + 'task': 'rero_ils.modules.notifications.tasks.process_notifications', + 'schedule': timedelta(minutes=15), + 'kwargs': { + 'notification_type': Notification.RECALL_NOTIFICATION_TYPE + }, + 'enabled': False, + }, 'claims-creation': { 'task': ('rero_ils.modules.items.tasks' '.process_late_claimed_issues'), diff --git a/rero_ils/modules/libraries/api.py b/rero_ils/modules/libraries/api.py index 5809162074..90980d5c1b 100644 --- a/rero_ils/modules/libraries/api.py +++ b/rero_ils/modules/libraries/api.py @@ -333,17 +333,6 @@ def get_timezone(self): default = pytz.timezone('Europe/Zurich') return default - def email_notification_type(self, notification_type): - """Get the email corresponding to the given notification type. - - :param notification_type: the notification type. - :return: the email corresponding to the notification type. - :rtype: string - """ - for setting in self['notification_settings']: - if setting['type'] == notification_type: - return setting['email'] - class LibrariesIndexer(IlsRecordsIndexer): """Holdings indexing class.""" @@ -356,3 +345,15 @@ def bulk_index(self, record_id_iterator): :param record_id_iterator: Iterator yielding record UUIDs. """ super().bulk_index(record_id_iterator, doc_type='lib') + + +def email_notification_type(libray, notification_type): + """Get the email corresponding to the given notification type. + + :param notification_type: the notification type. + :return: the email corresponding to the notification type. + :rtype: string + """ + for setting in libray['notification_settings']: + if setting['type'] == notification_type: + return setting['email'] diff --git a/rero_ils/modules/loans/api.py b/rero_ils/modules/loans/api.py index b2a6c991de..11a20d3a70 100644 --- a/rero_ils/modules/loans/api.py +++ b/rero_ils/modules/loans/api.py @@ -191,7 +191,7 @@ def check_required_params(self, action, **kwargs): required_params = self.action_required_params(action=action) missing_params = set(required_params) - set(kwargs) if missing_params: - message = 'Parameters {} are required'.format(missing_params) + message = f'Parameters {missing_params} are required' raise MissingRequiredParameterError(description=message) def update_pickup_location(self, pickup_location_pid): @@ -574,12 +574,7 @@ def create_notification(self, notification_type=None, counter=0): } notification = Notification.create( data=record, dbcommit=True, reindex=True) - enqueue = notification_type not in [ - Notification.RECALL_NOTIFICATION_TYPE, - Notification.AVAILABILITY_NOTIFICATION_TYPE - ] - # put into the queue only for batch notifications i.e. overdue - return notification.dispatch(enqueue=enqueue) + return notification @classmethod def concluded(cls, loan): diff --git a/rero_ils/modules/loans/cli.py b/rero_ils/modules/loans/cli.py index 51aeeba162..090f1ebb9c 100644 --- a/rero_ils/modules/loans/cli.py +++ b/rero_ils/modules/loans/cli.py @@ -36,6 +36,7 @@ from ..loans.api import Loan from ..locations.api import Location from ..notifications.api import Notification +from ..notifications.dispatcher import Dispatcher from ..notifications.tasks import create_notifications from ..patron_transaction_events.api import PatronTransactionEvent from ..patron_types.api import PatronType @@ -80,32 +81,52 @@ def create_loans(infile, verbose, debug): click.echo(msg) for transaction in range(loans.get('active', 0)): - item_barcode = create_loan(barcode, 'active', loanable_items, - verbose, debug) + item_barcode = create_loan( + barcode, 'active', + loanable_items, + verbose, + debug + ) errors_count = print_message(item_barcode, 'active', errors_count) for transaction in range(loans.get('overdue_active', 0)): - item_barcode = create_loan(barcode, 'overdue_active', - loanable_items, verbose, debug) + item_barcode = create_loan( + barcode, 'overdue_active', + loanable_items, + verbose, + debug + ) errors_count = print_message(item_barcode, 'overdue_active', errors_count) for transaction in range(loans.get('overdue_paid', 0)): - item_barcode = create_loan(barcode, 'overdue_paid', - loanable_items, verbose, debug) + item_barcode = create_loan( + barcode, 'overdue_paid', + loanable_items, + verbose, + debug + ) errors_count = print_message(item_barcode, 'overdue_paid', errors_count) for transaction in range(loans.get('extended', 0)): - item_barcode = create_loan(barcode, 'extended', loanable_items, - verbose, debug) + item_barcode = create_loan( + barcode, 'extended', + loanable_items, + verbose, + debug + ) errors_count = print_message(item_barcode, 'extended', errors_count) for transaction in range(loans.get('requested_by_others', 0)): - item_barcode = create_loan(barcode, 'requested_by_others', - loanable_items, verbose, debug) + item_barcode = create_loan( + barcode, 'requested_by_others', + loanable_items, + verbose, + debug + ) errors_count = print_message(item_barcode, 'requested_by_others', errors_count) @@ -128,14 +149,6 @@ def create_loans(infile, verbose, debug): errors_count = print_message(item_barcode, 'rank_2', errors_count) # create due soon notifications, overdue notifications are auto created. - result = create_notifications( - types=[ - Notification.DUE_SOON_NOTIFICATION_TYPE, - Notification.OVERDUE_NOTIFICATION_TYPE - ], - process=False, - verbose=verbose - ) # block given patron for patron_data in to_block: barcode = patron_data.get('barcode') @@ -149,6 +162,13 @@ def create_loans(infile, verbose, debug): ) for transaction_type, count in errors_count.items(): click.secho(f'Errors {transaction_type}: {count}', fg='red') + result = create_notifications( + types=[ + Notification.DUE_SOON_NOTIFICATION_TYPE, + Notification.OVERDUE_NOTIFICATION_TYPE + ], + verbose=verbose + ) click.echo(result) @@ -166,6 +186,7 @@ def print_message(item_barcode, transaction_type, errors_count): def create_loan(barcode, transaction_type, loanable_items, verbose=False, debug=False): """Create loans transactions.""" + notification_pids = [] try: item = next(loanable_items) patron = Patron.get_patron_by_barcode(barcode=barcode) @@ -191,7 +212,10 @@ def create_loan(barcode, transaction_type, loanable_items, verbose=False, dbcommit=True, reindex=True ) - loan.create_notification(notification_type='due_soon') + notification = loan.create_notification( + notification_type='due_soon') + if notification: + notification_pids.append(notification['pid']) end_date = datetime.now(timezone.utc) - timedelta(days=70) loan['end_date'] = end_date.isoformat() @@ -200,7 +224,10 @@ def create_loan(barcode, transaction_type, loanable_items, verbose=False, dbcommit=True, reindex=True ) - loan.create_notification(notification_type='overdue') + notification = loan.create_notification( + notification_type='overdue') + if notification: + notification_pids.append(notification['pid']) elif transaction_type == 'overdue_paid': end_date = datetime.now(timezone.utc) - timedelta(days=2) @@ -210,7 +237,10 @@ def create_loan(barcode, transaction_type, loanable_items, verbose=False, dbcommit=True, reindex=True ) - loan.create_notification(notification_type='due_soon') + notification = loan.create_notification( + notification_type='due_soon') + if notification: + notification_pids.append(notification['pid']) end_date = datetime.now(timezone.utc) - timedelta(days=70) loan['end_date'] = end_date.isoformat() @@ -219,9 +249,11 @@ def create_loan(barcode, transaction_type, loanable_items, verbose=False, dbcommit=True, reindex=True ) - notif = loan.create_notification(notification_type='overdue') - patron_transaction = [record - for record in notif.patron_transactions][0] + notification = notif = loan.create_notification( + notification_type='overdue') + if notification: + notification_pids.append(notification['pid']) + patron_transaction = next(notif.patron_transactions) user = get_random_librarian(patron).replace_refs() payment = create_payment_record( patron_transaction, @@ -266,14 +298,18 @@ def create_loan(barcode, transaction_type, loanable_items, verbose=False, requested_patron.pid, item), document_pid=extracted_data_from_ref(item.get('document')), ) - loan.create_notification(notification_type='recall') + notification = loan.create_notification( + notification_type='recall') + if notification: + notification_pids.append(notification['pid']) + Dispatcher.dispatch_notifications(notification_pids, verbose=verbose) return item['barcode'] except Exception as err: if verbose: click.secho(f'\tException loan {transaction_type}:{err}', fg='red') if debug: traceback.print_exc() - return None + return None, [] def create_request(barcode, transaction_type, loanable_items, verbose=False, diff --git a/rero_ils/modules/loans/listener.py b/rero_ils/modules/loans/listener.py index c372436704..c768c7b855 100644 --- a/rero_ils/modules/loans/listener.py +++ b/rero_ils/modules/loans/listener.py @@ -61,7 +61,8 @@ def listener_loan_state_changed(_, initial_loan, loan, trigger): and item.number_of_requests() == 0: send_notification_to_location(loan, item, item_location) elif loan['state'] == LoanState.ITEM_AT_DESK: - loan.create_notification(notification_type='availability') + notification = loan.create_notification( + notification_type='availability') # Create fees for checkin or extend operations if trigger in ['checkin', 'extend']: diff --git a/rero_ils/modules/monitoring.py b/rero_ils/modules/monitoring.py index 970f5b803b..1881782724 100644 --- a/rero_ils/modules/monitoring.py +++ b/rero_ils/modules/monitoring.py @@ -437,7 +437,7 @@ def info(cls, with_deleted=False, difference_db_es=False): Get count details for all records rest endpoints in json format. :param with_deleted: count also deleted items in database. - :return: dictionair with database, elasticsearch and databse minus + :return: dictionary with database, elasticsearch and databse minus elasticsearch count informations. """ info = {} @@ -472,7 +472,7 @@ def check(cls, with_deleted=False, difference_db_es=False): """Compaire elasticsearch with database counts. :param with_deleted: count also deleted items in database. - :return: dictionair with all document types with a difference in + :return: dictionary with all document types with a difference in databse and elasticsearch counts. """ checks = {} @@ -500,7 +500,7 @@ def missing(cls, doc_type, with_deleted=False): pids in elasticsearch. :param doc_type: doc type to get missing pids. - :return: dictionair with all missing pids. + :return: dictionary with all missing pids. """ missing_in_db, missing_in_es, pids_es_double, index =\ cls.get_es_db_missing_pids( @@ -600,3 +600,13 @@ def es_db_counts_cli(missing): def es_db_missing_cli(doc_type): """Print missing pids informations.""" Monitoring().print_missing(doc_type) + + +@monitoring.command('time_stamps') +@with_appcontext +def time_stamps_cli(): + """Print time_stampss informations.""" + for key, value in current_cache.get('timestamps').items(): + time = value.pop('time') + args = [f'{k}={v}' for k, v in value.items()] + click.echo(f'{time}: {key} {" | ".join(args)}') diff --git a/rero_ils/modules/notifications/api.py b/rero_ils/modules/notifications/api.py index 71b01062d5..810d567a79 100644 --- a/rero_ils/modules/notifications/api.py +++ b/rero_ils/modules/notifications/api.py @@ -20,19 +20,14 @@ from __future__ import absolute_import, print_function -from contextlib import contextmanager from copy import deepcopy from datetime import datetime, timedelta, timezone from functools import partial import ciso8601 -from celery import current_app as current_celery_app +from elasticsearch_dsl.query import Q from flask import current_app -from kombu import Exchange, Producer, Queue -from kombu.compat import Consumer -from sqlalchemy.orm.exc import NoResultFound -from .dispatcher import Dispatcher from .models import NotificationIdentifier, NotificationMetadata from ..api import IlsRecord, IlsRecordsIndexer, IlsRecordsSearch from ..circ_policies.api import DUE_SOON_REMINDER_TYPE, \ @@ -53,6 +48,7 @@ (Provider,), dict(identifier=NotificationIdentifier, pid_type='notif') ) + # notification minter notification_id_minter = partial(id_minter, provider=NotificationProvider) # notification fetcher @@ -80,18 +76,17 @@ class Notification(IlsRecord): AVAILABILITY_NOTIFICATION_TYPE = 'availability' DUE_SOON_NOTIFICATION_TYPE = 'due_soon' OVERDUE_NOTIFICATION_TYPE = 'overdue' + ALL_NOTIFICATIONS = [ + AVAILABILITY_NOTIFICATION_TYPE, + DUE_SOON_NOTIFICATION_TYPE, + OVERDUE_NOTIFICATION_TYPE, + RECALL_NOTIFICATION_TYPE + ] minter = notification_id_minter fetcher = notification_id_fetcher provider = NotificationProvider model_cls = NotificationMetadata - mq_routing_key = 'notification' - mq_exchange = Exchange(mq_routing_key, type='direct') - mq_queue = Queue( - mq_routing_key, - exchange=Exchange(mq_routing_key, type='direct'), - routing_key=mq_routing_key - ) @classmethod def create(cls, data, id_=None, delete_pid=False, @@ -104,11 +99,11 @@ def create(cls, data, id_=None, delete_pid=False, delete_pid=delete_pid) return record - def update_process_date(self): + def update_process_date(self, sent=False): """Update process date.""" - self['process_date'] = datetime.now(timezone.utc).isoformat() - self.update(data=self.dumps(), dbcommit=True, reindex=True) - return self + self['process_date'] = datetime.utcnow().isoformat() + self['notification_sent'] = sent + return self.update(data=self.dumps(), dbcommit=True, reindex=True) def replace_pids_and_refs(self): """Dumps data.""" @@ -141,7 +136,8 @@ def replace_pids_and_refs(self): next_open = library.next_open(keep_until) # language = data['loan']['patron']['communication_language'] next_open = next_open.strftime("%d.%m.%Y") - data['loan']['next_open'] = next_open + data['loan'][ + 'pickup_location']['library']['next_open'] = next_open else: data['loan']['pickup_location'] = \ self.transaction_location.replace_refs().dumps() @@ -172,10 +168,8 @@ def replace_pids_and_refs(self): # create a link to patron profile patron = Patron.get_record_by_pid(data['loan']['patron']['pid']) view_code = patron.get_organisation().get('code') - profile_url = '{base_url}/{view_code}/patrons/profile'.format( - base_url=current_app.config.get('RERO_ILS_APP_URL'), - view_code=view_code - ) + base_url = current_app.config.get('RERO_ILS_APP_URL') + profile_url = f'{base_url}/{view_code}/patrons/profile' data['loan']['profile_url'] = profile_url return data @@ -282,63 +276,6 @@ def patron_transactions(self): for result in results: yield PatronTransaction.get_record_by_pid(result.pid) - def dispatch(self, enqueue=True, verbose=False): - """Dispatch notification.""" - if enqueue: - with self.create_producer() as producer: - producer.publish(dict(pid=self.pid)) - else: - self = Dispatcher().dispatch_notification(notification=self, - verbose=verbose) - return self - - @contextmanager - def create_producer(self): - """Context manager that yields an instance of ``Producer``.""" - with current_celery_app.pool.acquire(block=True) as conn: - yield Producer( - conn, - exchange=self.mq_exchange, - routing_key=self.mq_routing_key, - auto_declare=True, - ) - - @classmethod - def process_notifications(cls, verbose=False): - """Process notifications queue.""" - count = {'send': 0, 'reject': 0, 'error': 0} - with current_celery_app.pool.acquire(block=True) as conn: - consumer = Consumer( - connection=conn, - queue=cls.mq_queue.name, - exchange=cls.mq_exchange.name, - routing_key=cls.mq_routing_key, - ) - - for message in consumer.iterqueue(): - payload = message.decode() - try: - pid = payload['pid'] - notification = Notification.get_record_by_pid(pid) - Dispatcher().dispatch_notification(notification, verbose) - message.ack() - count['send'] += 1 - except NoResultFound: - message.reject() - count['reject'] += 1 - except Exception: - message.reject() - current_app.logger.error( - "Failed to dispatch notification {pid}".format( - pid=payload.get('pid') - ), - exc_info=True - ) - count['error'] += 1 - consumer.close() - - return count - class NotificationsIndexer(IlsRecordsIndexer): """Holdings indexing class.""" @@ -370,6 +307,31 @@ def get_notification(loan, notification_type): return None +def get_notifications(notification_type, processed=False, not_sent=False): + """Returns specific notifications pids. + + :param processed: notifications are processed already. + """ + query = NotificationsSearch()\ + .filter('term', notification_type=notification_type) \ + .source('pid') + if not not_sent: + query = query.filter( + 'bool', must_not=[ + Q('exists', field='notification_sent'), + Q('term', notification_sent=False) + ] + ) + if processed: + query = query.filter('exists', field='process_date') + else: + query = query.filter( + 'bool', must_not=[Q('exists', field='process_date')]) + + for hit in query.scan(): + yield hit.pid + + def number_of_reminders_sent( loan, notification_type=Notification.OVERDUE_NOTIFICATION_TYPE): """Get the number of notifications sent for the given loan. @@ -407,7 +369,8 @@ def get_communication_channel_to_use(loan, notification_data, patron): reminder_type=reminder_type, idx=notification_data.get('reminder_counter', 0) ) - communication_channel = reminder.get('communication_channel') + if reminder: + communication_channel = reminder.get('communication_channel') # return the best communication channel if communication_channel == 'patron_setting': @@ -416,11 +379,12 @@ def get_communication_channel_to_use(loan, notification_data, patron): return communication_channel -def get_template_to_use(loan, notification_data): +def get_template_to_use(loan, notification_type, reminder_counter): """Get the template path to use for a notification. :param loan: the notification parent loan. - :param notification_data: the notification data. + :param notification_type: the notification type. + :param reminder_counter: the reminder counter. """ from ..loans.utils import get_circ_policy @@ -428,7 +392,6 @@ def get_template_to_use(loan, notification_data): # found into the related circulation policy. # TODO : depending of the communication channel, improve the function to # get the correct template. - notification_type = notification_data.get('notification_type') static_template_mapping = { Notification.RECALL_NOTIFICATION_TYPE: 'email/recall', Notification.AVAILABILITY_NOTIFICATION_TYPE: 'email/availability' @@ -444,9 +407,12 @@ def get_template_to_use(loan, notification_data): reminder_type = OVERDUE_REMINDER_TYPE reminder = cipo.get_reminder( reminder_type=reminder_type, - idx=notification_data.get('reminder_counter', 0) + idx=reminder_counter ) - return reminder.get('template') + template = f'email/{notification_type}' + if reminder: + template = reminder.get('template') + return template def calculate_notification_amount(notification): diff --git a/rero_ils/modules/notifications/cli.py b/rero_ils/modules/notifications/cli.py index 271159ad2f..072b8b1904 100644 --- a/rero_ils/modules/notifications/cli.py +++ b/rero_ils/modules/notifications/cli.py @@ -22,6 +22,7 @@ import click from flask.cli import with_appcontext +from .api import Notification from .tasks import process_notifications @@ -30,18 +31,38 @@ def notifications(): """Notification management commands.""" +@with_appcontext @notifications.command('process') -@click.option('--delayed', '-d', is_flag=True, default=False, - help='Run indexing in background.') -@click.option('-v', '--verbose', is_flag=True, default=False, - help='Verbose output') +@click.option('-t', '--type', 'notification_type', help="Notification Type.", + multiple=True, default=Notification.ALL_NOTIFICATIONS) +@click.option('-k', '--enqueue', 'enqueue', is_flag=True, default=False, + help="Enqueue record creation.") +@click.option('-v', '--verbose', 'verbose', is_flag=True, default=False, + help='verbose') @with_appcontext -def process(delayed, verbose): +def process(notification_type, enqueue, verbose): """Process notifications.""" - click.secho('Process notifications:', fg='green') - if delayed: - uid = process_notifications.delay(verbose=verbose) - msg = f'Started task: {uid}' - else: - msg = process_notifications(verbose=verbose) - click.echo(msg) + results = {} + enqueue_results = {} + for n_type in notification_type: + if n_type not in Notification.ALL_NOTIFICATIONS: + click.secho( + f'Notification type does not exist: {n_type}', fg='red') + break + click.secho( + f'Process notification: {n_type}', fg='green') + if enqueue: + enqueue_results[n_type] = process_notifications.delay( + notification_type=n_type, verbose=verbose) + else: + results[n_type] = process_notifications( + notification_type=n_type, verbose=verbose) + + if verbose: + if enqueue_results: + for key, value in enqueue_results.items(): + results[key] = value.get() + + for key, value in results.items(): + result_values = ' '.join([f'{k}={v}' for k, v in value.items()]) + click.secho(f'Notification {key:12}: {result_values}') diff --git a/rero_ils/modules/notifications/dispatcher.py b/rero_ils/modules/notifications/dispatcher.py index aa77a5da3c..07e3c1fb82 100644 --- a/rero_ils/modules/notifications/dispatcher.py +++ b/rero_ils/modules/notifications/dispatcher.py @@ -19,127 +19,245 @@ from __future__ import absolute_import, print_function +import pycountry from flask import current_app from invenio_mail.api import TemplatedMessage from invenio_mail.tasks import send_email as task_send_email +from num2words import num2words -from ..locations.api import Location +from ..libraries.api import email_notification_type class Dispatcher: """Dispatcher notifications class.""" - def dispatch_notification(self, notification=None, verbose=False): - """Dispatch the notification.""" - from .api import get_communication_channel_to_use + @classmethod + def dispatch_notifications(cls, notification_pids=[], resend=False, + verbose=False): + """Dispatch the notification. + + :param notification_pids: Notification pids to send notification. + :param resend: Resend notification if already send. + :param verbose: Verbose output. + :returns: ictionair with proccessed and send count + """ + from .api import Notification, get_communication_channel_to_use, \ + get_template_to_use def not_yet_implemented(*args): """Do nothing placeholder for a notification.""" return - if notification: - data = notification.replace_pids_and_refs() - communication_switcher = { - 'email': Dispatcher.send_mail_to_patron, - 'mail': Dispatcher.send_mail_for_printing, - # 'sms': not_yet_implemented - # 'telepathy': self.madness_mind - # ... - } - patron = data['loan']['patron'] - dispatcher_function = communication_switcher.get( - get_communication_channel_to_use( - notification.init_loan(), notification, patron - ), - not_yet_implemented - ) - if dispatcher_function == not_yet_implemented: - current_app.logger.warning( - 'The communication channel of the patron (pid: {pid})' - 'is not yet implemented'.format(pid=patron['pid']) + sent = not_sent = count = 0 + aggregated = {} + for notification_pid in notification_pids: + count += 1 + notification = Notification.get_record_by_pid(notification_pid) + if notification: + process_date = notification.get('process_date') + if process_date: + current_app.logger.warning( + f'Notification: {notification.pid} already processed ' + f'on: {process_date}' + ) + if not resend: + continue + communication_switcher = { + 'email': Dispatcher.send_mail_to_patron, + 'mail': Dispatcher.send_mail_for_printing, + # 'sms': not_yet_implemented + # 'telepathy': self.madness_mind + # ... + } + data = notification.replace_pids_and_refs() + loan = data['loan'] + patron = loan['patron'] + communication_channel = get_communication_channel_to_use( + loan, notification, patron ) - dispatcher_function(data) - notification = notification.update_process_date() - if verbose: - current_app.logger.info( - ('Notification: {pid} chanel: {chanel} type:' - '{type} loan: {lpid}').format( - pid=notification['pid'], - chanel=patron['patron']['communication_channel'], - type=notification['notification_type'], - lpid=data['loan']['pid'] + dispatcher_function = communication_switcher.get( + communication_channel, + not_yet_implemented + ) + reminder_counter = data.get('reminder_counter', 0) + reminder = reminder_counter + 1 + communication_lang = patron['patron']["communication_language"] + try: + language = pycountry.languages.get( + bibliographic=communication_lang) + reminder = num2words( + reminder, + to='ordinal_num', + lang=language.alpha_2 ) + except Exception: + pass + if dispatcher_function == not_yet_implemented: + current_app.logger.warning( + 'The communication channel of the patron' + f' (pid: {patron["pid"]}) is not yet implemented') + # loan = Loan.get_record_by_pid(loan['pid']) + notification_type = notification['notification_type'] + tpl_path = get_template_to_use( + loan, notification_type, reminder_counter).rstrip('/') + template = f'{tpl_path}/{communication_lang}.txt' + # Add all information used in the templates here: + ctx_data = { + 'template': template, + 'profile_url': loan['profile_url'], + 'patron': patron, + 'library': { + 'pid': loan['library']['pid'], + 'notification_email': email_notification_type( + loan['library'], notification_type), + 'email': loan['library'].get('email'), + 'name': loan['library']['name'], + 'address': loan['library']['address'], + 'notification_settings': loan['library'][ + 'notification_settings'] + }, + 'documents': [], + 'notifications': [] + } + pickup_location = loan.get('pickup_location') + if pickup_location: + ctx_data['pickup_library'] = pickup_location.get('library') + + # aggregate notifications + n_type = notification_type + l_pid = loan['library']['pid'] + p_pid = patron['pid'] + aggregated.setdefault(n_type, {}) + aggregated[n_type].setdefault(l_pid, {}) + aggregated[n_type][l_pid].setdefault(p_pid, ctx_data) + documents_data = { + 'title_text': loan['document']['title_text'], + 'responsibility_statement': + loan['document']['responsibility_statement'], + 'reminder': reminder + } + end_date = loan.get('end_date') + if end_date: + documents_data['end_date'] = end_date + aggregated[n_type][l_pid][p_pid]['documents'].append( + documents_data ) - return notification + aggregated[n_type][l_pid][p_pid]['notifications'].append( + notification + ) + + for notification_type, notification_values in aggregated.items(): + for library_pid, library_values in notification_values.items(): + for patron_pid, ctx_data in library_values.items(): + if verbose: + current_app.logger.info( + f'Dispatch notifications: {notification_type} ' + f'library: {library_pid} ' + f'patron: {patron_pid} ' + f'documents: {len(ctx_data["documents"])}' + ) + sent = dispatcher_function(ctx_data) + for notification in ctx_data['notifications']: + notification.update_process_date(sent=sent) + if sent: + sent += len(ctx_data['notifications']) + else: + not_sent += len(ctx_data['notifications']) + return {'processed': count, 'sent':sent, 'not_sent': not_sent} @staticmethod - def _create_email(data, patron, library, recipients): - """.""" - from flask_babelex import Locale - - from .api import get_template_to_use - from ..loans.api import Loan - - language = patron['patron']['communication_language'] - # set the current language for translations in template - with current_app.test_request_context() as ctx: - ctx.babel_locale = Locale.parse(language) - loan = Loan.get_record_by_pid(data['loan']['pid']) - tpl_path = get_template_to_use(loan, data).rstrip('/') - template = f'{tpl_path}/{language}.txt' - # get the sender email from - # loan.pickup_location_pid.location.library.email - sender = library['email'] - msg = TemplatedMessage( - template_body=template, - sender=sender, - recipients=recipients, - ctx=data['loan'] - ) - text = msg.body.split('\n') - # subject is the first line - msg.subject = text[0] - # body - msg.body = '\n'.join(text[1:]) - return msg + def _create_email(recipients, sender, ctx_data, template): + """Create email message from template. + + :param recipients: List of emails to send the message too. + :param sender: Sender email address. + :param ctx_data: Dictionary with informations used in template. + :param template: Template to use to create TemplatedMessage. + :returns: Message created. + """ + msg = TemplatedMessage( + template_body=template, + sender=sender, + recipients=recipients, + ctx=ctx_data + ) + text = msg.body.split('\n') + # subject is the first line + msg.subject = text[0] + # body + msg.body = '\n'.join(text[1:]) + return msg @staticmethod - def send_mail_for_printing(data): - """Send the notification by email.""" - patron = data['loan']['patron'] - library = Location.get_record_by_pid( - data['loan']['pickup_location_pid']).get_library() + def send_mail_for_printing(ctx_data): + """Send the notification by email to the library. + + :param ctx_data: Dictionary with informations used in template. + """ # get the recipient email from the library - email = library.email_notification_type(data['notification_type']) - if not email: + notification_email = ctx_data['library'].get('notification_email') + error_reason = '' + if not notification_email: + error_reason = '(Missing notification email)' + sender = ctx_data['library'].get('email') + if not sender: + error_reason = '(Missing sender email)' + if error_reason: current_app.logger.warning( - f"Notification is lost for patron(patron['pid'])." - ) - return - msg = Dispatcher._create_email(data, patron, library, [email]) - task_send_email.delay(msg.__dict__) + 'Notification for printing is lost for patron: ' + f'{ctx_data["patron"]["pid"]} ' + f'send to library: {ctx_data["library"]["pid"]} ' + f'{error_reason}') + return False + msg = Dispatcher._create_email( + recipients=[notification_email], + sender=sender, + ctx_data=ctx_data, + template=ctx_data['template'] + ) + task_send_email.apply_async((msg.__dict__,)) + return True @staticmethod - def send_mail_to_patron(data): - """Send the notification by email to the patron.""" - patron = data['loan']['patron'] - library = Location.get_record_by_pid( - data['loan']['pickup_location_pid']).get_library() + def send_mail_to_patron(ctx_data): + """Send the notification by email to the patron. + + :param ctx_data: Dictionary with informations used in template. + """ # get the recipient email from loan.patron.patron.email - recipients = [patron.get('email')] + error_reason = '' + recipients = [ctx_data['patron'].get('email')] # additional recipient - add_recipient = patron['patron'].get('additional_communication_email') + add_recipient = ctx_data['patron'].get( + 'additional_communication_email') if add_recipient: - recipents.push(add_recipient) + recipients.append(add_recipient) + if not recipients: + error_reason = '(Missing notification recipients)' + sender = ctx_data['library'].get('email') + if not sender: + error_reason = '(Missing sender email)' + if error_reason: + current_app.logger.warning( + 'Notification is lost for patron: ' + f'{ctx_data["patron"]["pid"]} ' + f'send to library: {ctx_data["library"]["pid"]} ' + f'{error_reason}') + return False # delay delay_availability = 0 # get notification settings for notification type - notification_type = data['notification_type'] - for setting in library['notification_settings']: + for setting in ctx_data['library']['notification_settings']: if setting['type'] == 'availability': - delay_availability = setting['delay'] - msg = Dispatcher._create_email(data, patron, library, recipients) + delay_availability = setting.get('delay', 0) + msg = Dispatcher._create_email( + recipients=recipients, + sender=sender, + ctx_data=ctx_data, + template=ctx_data['template'] + ) task_send_email.apply_async( (msg.__dict__,), countdown=delay_availability ) + return True diff --git a/rero_ils/modules/notifications/jsonschemas/notifications/notification-v0.0.1.json b/rero_ils/modules/notifications/jsonschemas/notifications/notification-v0.0.1.json index 1df4419440..47ac6d4762 100644 --- a/rero_ils/modules/notifications/jsonschemas/notifications/notification-v0.0.1.json +++ b/rero_ils/modules/notifications/jsonschemas/notifications/notification-v0.0.1.json @@ -44,6 +44,10 @@ "format": "date-time", "title": "Notification processing date" }, + "notification_sent": { + "type": "boolean", + "title": "Notification sent or not" + }, "reminder_counter": { "type": "integer", "title": "Current reminder count" diff --git a/rero_ils/modules/notifications/mappings/v7/notifications/notification-v0.0.1.json b/rero_ils/modules/notifications/mappings/v7/notifications/notification-v0.0.1.json index b1b6361259..57142cec15 100644 --- a/rero_ils/modules/notifications/mappings/v7/notifications/notification-v0.0.1.json +++ b/rero_ils/modules/notifications/mappings/v7/notifications/notification-v0.0.1.json @@ -15,6 +15,9 @@ "process_date": { "type": "date" }, + "notification_sent": { + "type": "boolean" + }, "reminder_counter": { "type": "integer" }, diff --git a/rero_ils/modules/notifications/tasks.py b/rero_ils/modules/notifications/tasks.py index c2e486a3d8..2f77375863 100644 --- a/rero_ils/modules/notifications/tasks.py +++ b/rero_ils/modules/notifications/tasks.py @@ -24,34 +24,37 @@ from celery import shared_task from flask import current_app -from .api import Notification +from .api import Notification, get_notifications +from .dispatcher import Dispatcher from ..circ_policies.api import OVERDUE_REMINDER_TYPE from ..libraries.api import Library from ..loans.api import get_due_soon_loans, get_overdue_loans from ..utils import set_timestamp -@shared_task(ignore_result=True) -def process_notifications(verbose=False): - """Process notifications.""" - result = Notification.process_notifications(verbose=verbose) - msg = '{info}| send: {send} reject: {reject} error: {error}'.format( - info='notifications', - send=result['send'], - reject=result['reject'], - error=result['error'] +@shared_task() +def process_notifications(notification_type, verbose=True): + """Dispatch notifications. + + :param notification_type: notification type to dispatch the notifications. + :param verbose: is the task should be verbose. + """ + notification_pids = get_notifications(notification_type=notification_type) + result = Dispatcher.dispatch_notifications( + notification_pids=notification_pids, + verbose=verbose ) - return msg + set_timestamp(f'notification-dispatch-{notification_type}', **result) + return result -@shared_task(ignore_result=True) -def create_notifications(types=None, tstamp=None, process=True, verbose=True): +@shared_task() +def create_notifications(types=None, tstamp=None, verbose=True): """Creates requested notifications. :param types: an array of notification types to create. :param tstamp: a timestamp to specify when the function is execute. By default it will be `datetime.now()`. - :param process: is the notifications should be processed/sent. :param verbose: is the task should be verbose. """ from ..loans.utils import get_circ_policy @@ -64,10 +67,11 @@ def create_notifications(types=None, tstamp=None, process=True, verbose=True): if Notification.DUE_SOON_NOTIFICATION_TYPE in types: due_soon_type = Notification.DUE_SOON_NOTIFICATION_TYPE notification_counter[due_soon_type] = 0 - logger.debug("OVERDUE_NOTIFICATION_CREATION --------------") + logger.debug("DUE_SOON_NOTIFICATION_TYPE --------------") for loan in get_due_soon_loans(tstamp=tstamp): logger.debug(f'* Loan#{loan.pid} is considerate as \'due_soon\'') - loan.create_notification(notification_type=due_soon_type) + notification = loan.create_notification( + notification_type=due_soon_type) notification_counter[due_soon_type] += 1 # OVERDUE NOTIFICATIONS @@ -103,19 +107,19 @@ def create_notifications(types=None, tstamp=None, process=True, verbose=True): if notification: logger.debug(f' --> Overdue notification#{idx+1} created') notification_counter[overdue_type] += 1 + else: logger.debug(f' --> Overdue notification#{idx+1} skipped ' f':: already sent') + notification_sum = sum(notification_counter.values()) + counters = {k: v for k, v in notification_counter.items() if v > 0} if verbose: logger = current_app.logger logger.info("NOTIFICATIONS CREATION TASK") - notification_sum = sum(notification_counter.values()) logger.info(f' * total of {notification_sum} notification(s) created') - counters = {k: v for k, v in notification_counter.items() if v > 0} for notif_type, cpt in counters.items(): logger.info(f' +--> {cpt} `{notif_type}` notification(s) created') - if process: - logger.info(process_notifications.run(verbose=verbose)) - set_timestamp('notification-creation') + set_timestamp('notification-creation', **counters) + return counters diff --git a/rero_ils/modules/notifications/templates/email/_patron_address.txt b/rero_ils/modules/notifications/templates/email/_patron_address.txt index f3094435a7..9cc1a72556 100644 --- a/rero_ils/modules/notifications/templates/email/_patron_address.txt +++ b/rero_ils/modules/notifications/templates/email/_patron_address.txt @@ -1,4 +1,4 @@ -{%- if patron.patron.communication_channel == 'mail' %} +{%- if patron.communication_channel == 'mail' %} {{ patron.first_name }} {{ patron.last_name }} {% if patron.street %}{{ patron.street }}{% endif %} {% if patron.postal_code %}{{ patron.postal_code }}{% endif %} {% if patron.city %}{{ patron.city }}{% endif %} diff --git a/rero_ils/modules/notifications/templates/email/availability/eng.txt b/rero_ils/modules/notifications/templates/email/availability/eng.txt index 92380df4b5..85f3116b5b 100644 --- a/rero_ils/modules/notifications/templates/email/availability/eng.txt +++ b/rero_ils/modules/notifications/templates/email/availability/eng.txt @@ -3,15 +3,18 @@ Invitation to pick up a document Dear patron, The document you requested is now available. You can pick it up at the loan desk of the library mentioned below. +{%- for document in documents %} Title: {{ document.title_text }} / {{ document.responsibility_statement }} -Pick up location: {{ pickup_location.library.name }} -To pick up until: {{ next_open }} +Pick up location: {{ pickup_library.name }} +To pick up until: {{ pickup_library.next_open }} +{%- endfor %} + Should the document not be picked up within the given period, it will be made available for other people. You can consult your account and extend the loan period of your documents at: {{ profile_url }} Best regards -{{ pickup_location.library.name }} -{{ pickup_location.library.address }} +{{ library.name }} +{{ library.address }} diff --git a/rero_ils/modules/notifications/templates/email/availability/fre.txt b/rero_ils/modules/notifications/templates/email/availability/fre.txt index 66fcd374c2..860641c027 100644 --- a/rero_ils/modules/notifications/templates/email/availability/fre.txt +++ b/rero_ils/modules/notifications/templates/email/availability/fre.txt @@ -3,15 +3,17 @@ Invitation à retirer un document Chère lectrice, cher lecteur, Le document que vous avez demandé est maintenant disponible. Vous pouvez venir le retirer au bureau de prêt de la bibliothèque mentionnée ci-dessous. +{%- for document in documents %} Titre : {{ document.title_text }} / {{ document.responsibility_statement }} -Lieu de retrait : {{ pickup_location.library.name }} -A retirer jusqu'au : {{ next_open }} +Lieu de retrait : {{ pickup_library.name }} +A retirer jusqu'au : {{ pickup_library.next_open }} +{%- endfor %} Si le document n'est pas retiré dans les délais, il sera remis en circulation pour d'autres personnes. Vous pouvez consulter votre compte et prolonger la durée de prêt de vos documents à l'adresse : {{ profile_url }} Avec nos compliments -{{ pickup_location.library.name }} -{{ pickup_location.library.address }} +{{ library.name }} +{{ library.address }} diff --git a/rero_ils/modules/notifications/templates/email/availability/ger.txt b/rero_ils/modules/notifications/templates/email/availability/ger.txt index 687a77cfb4..e3b8c39589 100644 --- a/rero_ils/modules/notifications/templates/email/availability/ger.txt +++ b/rero_ils/modules/notifications/templates/email/availability/ger.txt @@ -3,15 +3,17 @@ Abholeinladung Sehr geehrte Leserin, sehr geehrter Leser, Das von Ihnen bestellte Dokument ist nun verfügbar und kann an der Ausleihtheke der nachstehend genannten Bibliothek abgeholt werden. +{%- for document in documents %} Titel : {{ document.title_text }} / {{ document.responsibility_statement }} -Abholort: {{ pickup_location.library.name }} -Abholen bis: {{ next_open }} +Abholort: {{ pickup_library.name }} +Abholen bis: {{ pickup_library.next_open }} +{%- endfor %} Wenn das Dokument innerhalb der gegebenen Frist nicht abgeholt wird, wird es anderen Personen zur Verfügung gestellt. Unter folgender Adresse können Sie Ihr Konto einsehen und die Ausleihfrist Ihrer Dokumente verlängern: {{ profile_url }} Freundliche Grüsse -{{ pickup_location.library.name }} -{{ pickup_location.library.address }} +{{ library.name }} +{{ library.address }} diff --git a/rero_ils/modules/notifications/templates/email/availability/ita.txt b/rero_ils/modules/notifications/templates/email/availability/ita.txt index cf59914e38..8e7480c9af 100644 --- a/rero_ils/modules/notifications/templates/email/availability/ita.txt +++ b/rero_ils/modules/notifications/templates/email/availability/ita.txt @@ -3,15 +3,17 @@ Invito a ritirare un documento Cara lettrice, caro lettore, Il documento che Lei ha domandato è ora disponibile. Lei può ritirarlo al servizio prestiti della biblioteca sotto indicata. +{%- for document in documents %} Titolo: {{ document.title_text }} / {{ document.responsibility_statement }} -Punto di ritiro: {{ pickup_location.library.name }} -Ritirare entro: {{ next_open }} +Punto di ritiro: {{ pickup_library.name }} +Ritirare entro: {{ pickup_library.next_open }} +{%- endfor %} Se il documento non è ritirato entro detto termine, esso sarà rimesso in circolazione per altre persone. Lei può consultare il Suo conto et prorogare la durata di prestito dei Suoi documenti al seguente indirizzo: {{ profile_url }} Cordiali saluti -{{ pickup_location.library.name }} -{{ pickup_location.library.address }} +{{ library.name }} +{{ library.address }} diff --git a/rero_ils/modules/notifications/templates/email/due_soon/eng.txt b/rero_ils/modules/notifications/templates/email/due_soon/eng.txt index 176c4e4e0a..f3f459c22f 100644 --- a/rero_ils/modules/notifications/templates/email/due_soon/eng.txt +++ b/rero_ils/modules/notifications/templates/email/due_soon/eng.txt @@ -4,8 +4,10 @@ Dear patron, The loan period of following documents is expiring: +{%- for document in documents %} Title : {{ document.title_text }} / {{ document.responsibility_statement }} -Due date: {{ end_date }} +Due date: {{ document.end_date }} +{%- endfor %} You can consult your account and extend the loan period of your documents at: {{ profile_url }} diff --git a/rero_ils/modules/notifications/templates/email/due_soon/fre.txt b/rero_ils/modules/notifications/templates/email/due_soon/fre.txt index c1bb3662e3..24d54b5d84 100644 --- a/rero_ils/modules/notifications/templates/email/due_soon/fre.txt +++ b/rero_ils/modules/notifications/templates/email/due_soon/fre.txt @@ -5,8 +5,10 @@ Chère lectrice, cher lecteur, Le délai de prêt des documents mentionnés ci-dessous arrive à échéance : +{%- for document in documents %} Titre : {{ document.title_text }} / {{ document.responsibility_statement }} -Echéance : {{ end_date }} +Echéance : {{ document.end_date }} +{%- endfor %} Vous pouvez consulter votre compte et prolonger la durée de prêt de vos documents à l'adresse : {{ profile_url }} diff --git a/rero_ils/modules/notifications/templates/email/due_soon/ger.txt b/rero_ils/modules/notifications/templates/email/due_soon/ger.txt index c869b6a02f..755072f374 100644 --- a/rero_ils/modules/notifications/templates/email/due_soon/ger.txt +++ b/rero_ils/modules/notifications/templates/email/due_soon/ger.txt @@ -4,8 +4,10 @@ Sehr geehrte Leserin, sehr geehrter Leser, Die Ausleihfrist der folgenden Dokumente läuft ab: +{%- for document in documents %} Titel: {{ document.title_text }} / {{ document.responsibility_statement }} -Rückgabedatum: {{ end_date }} +Rückgabedatum: {{ document.end_date }} +{%- endfor %} Unter folgender Adresse können Sie Ihr Konto einsehen und die Ausleihfrist Ihrer Dokumente verlängern: {{ profile_url }} diff --git a/rero_ils/modules/notifications/templates/email/due_soon/ita.txt b/rero_ils/modules/notifications/templates/email/due_soon/ita.txt index 031addccb8..a58b463477 100644 --- a/rero_ils/modules/notifications/templates/email/due_soon/ita.txt +++ b/rero_ils/modules/notifications/templates/email/due_soon/ita.txt @@ -4,8 +4,10 @@ Cara lettrice, caro lettore, La durata di prestito dei seguenti documenti sta per scadere: +{%- for document in documents %} Titolo : {{ document.title_text }} / {{ document.responsibility_statement }} -Scadenza: {{ end_date }} +Scadenza: {{ document.end_date }} +{%- endfor %} Lei può consultare il Suo conto et prorogare la durata di prestito dei Suoi documenti al seguente indirizzo: {{ profile_url }} diff --git a/rero_ils/modules/notifications/templates/email/others/location_notification.txt b/rero_ils/modules/notifications/templates/email/others/location_notification.txt index 4cb02323b4..721fda8298 100644 --- a/rero_ils/modules/notifications/templates/email/others/location_notification.txt +++ b/rero_ils/modules/notifications/templates/email/others/location_notification.txt @@ -2,7 +2,7 @@ The item [{{ item.barcode }}] has been requested {%- include('email/_patron_address.txt') %} An item has been requested. See below for information about this item. -Pickup location : {{ pickup_location.library.name }} -- {{ pickup_location.name }} +Pickup location : {{ pickup_location.name }} -- {{ pickup_location.name }} Request date : {{ loan.transaction_date }} Item barcode : {{ item.barcode }} Item call number : {{ item.call_number }} diff --git a/rero_ils/modules/notifications/templates/email/overdue/eng.txt b/rero_ils/modules/notifications/templates/email/overdue/eng.txt index 26ce68fb71..4c278d3eff 100644 --- a/rero_ils/modules/notifications/templates/email/overdue/eng.txt +++ b/rero_ils/modules/notifications/templates/email/overdue/eng.txt @@ -3,9 +3,11 @@ Dear patron, The loan period of following documents has expired: +{%- for document in documents %} Title: {{ document.title_text }} / {{ document.responsibility_statement }} -Due date: {{ end_date }} -Note: 1st reminder +Due date: {{ document.end_date }} +Note: {{ document.reminder }} reminder +{%- endfor %} You can consult your account and extend the loan period of your documents at: {{ profile_url }} diff --git a/rero_ils/modules/notifications/templates/email/overdue/fre.txt b/rero_ils/modules/notifications/templates/email/overdue/fre.txt index 9d0c4e67e9..22e5ad324d 100644 --- a/rero_ils/modules/notifications/templates/email/overdue/fre.txt +++ b/rero_ils/modules/notifications/templates/email/overdue/fre.txt @@ -4,9 +4,11 @@ Chère lectrice, cher lecteur, La durée du prêt des documents mentionnés ci-dessous est échue : +{%- for document in documents %} Titre : {{ document.title_text }} / {{ document.responsibility_statement }} -Echéance : {{ end_date }} -Note : 1er rappel +Echéance : {{ document.end_date }} +Note : {{ document.reminder }} rappel +{%- endfor %} Vous pouvez consulter votre compte et prolonger la durée de prêt de vos documents à l'adresse : {{ profile_url }} diff --git a/rero_ils/modules/notifications/templates/email/overdue/ger.txt b/rero_ils/modules/notifications/templates/email/overdue/ger.txt index 4e4024093b..7c55b964d7 100644 --- a/rero_ils/modules/notifications/templates/email/overdue/ger.txt +++ b/rero_ils/modules/notifications/templates/email/overdue/ger.txt @@ -4,9 +4,11 @@ Sehr geehrte Leserin, sehr geehrter Leser, Die Ausleihfrist folgender Dokumente ist abgelaufen: +{%- for document in documents %} Titel : {{ document.title_text }} / {{ document.responsibility_statement }} -Rückgabedatum: {{ end_date }} -Anmerkung: 1. Mahnung +Rückgabedatum: {{ document.end_date }} +Anmerkung: {{ document.reminder }} Mahnung +{%- endfor %} Unter folgender Adresse können Sie Ihr Konto einsehen und die Ausleihfrist Ihrer Dokumente verlängern: {{ profile_url }} diff --git a/rero_ils/modules/notifications/templates/email/overdue/ita.txt b/rero_ils/modules/notifications/templates/email/overdue/ita.txt index fa6c6d38df..0db488aebd 100644 --- a/rero_ils/modules/notifications/templates/email/overdue/ita.txt +++ b/rero_ils/modules/notifications/templates/email/overdue/ita.txt @@ -4,9 +4,11 @@ Cara lettrice, caro lettore, La durata di prestito dei seguenti documenti è scaduta: +{%- for document in documents %} Titolo : {{ document.title_text }} / {{ document.responsibility_statement }} -Scadenza: {{ end_date }} -NoNotate: 1° richiamo +Scadenza: {{ document.end_date }} +NoNotate: {{ document.reminder }} richiamo +{%- endfor %} Lei può consultare il Suo conto et prorogare la durata di prestito dei Suoi documenti al seguente indirizzo: {{ profile_url }} diff --git a/rero_ils/modules/notifications/templates/email/recall/eng.txt b/rero_ils/modules/notifications/templates/email/recall/eng.txt index 668ad6146b..df1ca6aa29 100644 --- a/rero_ils/modules/notifications/templates/email/recall/eng.txt +++ b/rero_ils/modules/notifications/templates/email/recall/eng.txt @@ -4,9 +4,11 @@ Dear patron, The document you borrowed has been requested by another person. An extension of the loan period is therefore no longer possible and we kindly ask you to return it at the latest by the due date. +{%- for document in documents %} Title : {{ document.title_text }} / {{ document.responsibility_statement }} -Due date: {{ end_date }} +Due date: {{ document.end_date }} Note: Non extendable +{%- endfor %} You can consult your account at: {{ profile_url }} diff --git a/rero_ils/modules/notifications/templates/email/recall/fre.txt b/rero_ils/modules/notifications/templates/email/recall/fre.txt index 98391fb9ac..9c06517a57 100644 --- a/rero_ils/modules/notifications/templates/email/recall/fre.txt +++ b/rero_ils/modules/notifications/templates/email/recall/fre.txt @@ -4,9 +4,11 @@ Chère lectrice, cher lecteur, Le document que vous avez emprunté vient d'être réservé par une autre personne. D'ores et déjà nous vous signalons que nous ne pourrons pas le prolonger et vous demandons de bien vouloir le restituer au plus tard à la date d'échéance. +{%- for document in documents %} Titre : {{ document.title_text }} / {{ document.responsibility_statement }} -Échéance : {{ end_date }} +Échéance : {{ document.end_date }} Note : Non prolongeable +{%- endfor %} Vous pouvez consulter votre compte à l'adresse : {{ profile_url }} diff --git a/rero_ils/modules/notifications/templates/email/recall/ger.txt b/rero_ils/modules/notifications/templates/email/recall/ger.txt index f9198859e0..8459342f76 100644 --- a/rero_ils/modules/notifications/templates/email/recall/ger.txt +++ b/rero_ils/modules/notifications/templates/email/recall/ger.txt @@ -4,9 +4,11 @@ Sehr geehrte Leserin, sehr geehrter Leser, Das von Ihnen ausgeliehene Dokument ist von einer anderen Person reserviert worden. Eine Verlängerung der Ausleihfrist ist deshalb nicht mehr möglich und wir bitten Sie, das Dokument spätestens bis zum Rückgabedatum zurückzugeben. +{%- for document in documents %} Titel : {{ document.title_text }} / {{ document.responsibility_statement }} -Rückgabedatum: {{ end_date }} +Rückgabedatum: {{ document.end_date }} Anmerkung: Nicht verlängerbar +{%- endfor %} Unter folgender Adresse können Sie Ihr Konto einsehen: {{ profile_url }} diff --git a/rero_ils/modules/notifications/templates/email/recall/ita.txt b/rero_ils/modules/notifications/templates/email/recall/ita.txt index bffe736179..fc3f213dce 100644 --- a/rero_ils/modules/notifications/templates/email/recall/ita.txt +++ b/rero_ils/modules/notifications/templates/email/recall/ita.txt @@ -3,10 +3,12 @@ Documento non prorogabile Cara lettrice, caro lettore, Il documento che Lei ha presto in prestito è stato riservato da un'altra persona. Una proroga della durata di prestito non è quindi più possibile e La preghiamo di restituirlo entro la scadenza. +{%- for document in documents %} Title : {{ document.title_text }} / {{ document.responsibility_statement }} -Scadenza: {{ end_date }} +Scadenza: {{ document.end_date }} Nota: Non prorogabile +{%- endfor %} Lei può consultare il Suo conto al seguente indirizzo: {{ profile_url }} diff --git a/scripts/setup b/scripts/setup index c486859841..e24815a094 100755 --- a/scripts/setup +++ b/scripts/setup @@ -443,7 +443,6 @@ eval ${PREFIX} invenio fixtures create_loans ${DATA_PATH}/loans.json # process notifications eval ${PREFIX} invenio notifications process - # create token access for monitoring # if the environement variable INVENIO_RERO_ACCESS_TOKEN_MONITORING is not set # a new token will be generated @@ -476,7 +475,8 @@ then info_msg "Start OAI harvesting asynchrone" eval ${PREFIX} invenio oaiharvester harvest -n ebooks -q -k else - eval ${PREFIX} invenio scheduler enable_tasks -n scheduler-timestamp -n bulk-indexer -n anonymize-loans -n claims-creation -n notification-creation -n accounts -n clear_and_renew_subscriptions -v + eval ${PREFIX} invenio scheduler enable_tasks -n scheduler-timestamp -n bulk-indexer -n anonymize-loans -n claims-creation -n accounts -n clear_and_renew_subscriptions -v + eval ${PREFIX} invenio scheduler enable_tasks -n notification-creation -n notification-dispatch-due_soon -n notification-dispatch-overdue -n notification-dispatch-availability -n notification-dispatch-recall -v info_msg "For ebooks harvesting run:" msg "\tinvenio oaiharvester harvest -n ebooks -a max=100 -q" fi diff --git a/tests/api/circulation/test_borrow_limits.py b/tests/api/circulation/test_borrow_limits.py index 61804f2288..1525726d0e 100644 --- a/tests/api/circulation/test_borrow_limits.py +++ b/tests/api/circulation/test_borrow_limits.py @@ -27,6 +27,7 @@ from rero_ils.modules.loans.api import Loan, LoanAction, get_overdue_loans from rero_ils.modules.notifications.api import Notification, \ NotificationsSearch, number_of_reminders_sent +from rero_ils.modules.notifications.dispatcher import Dispatcher from rero_ils.modules.patron_types.api import PatronType from rero_ils.modules.utils import get_ref_for_pid @@ -234,8 +235,9 @@ def test_overdue_limit( assert overdue_loans[0].get('pid') == loan_pid assert number_of_reminders_sent(loan) == 0 - loan.create_notification( + notification = loan.create_notification( notification_type=Notification.OVERDUE_NOTIFICATION_TYPE) + Dispatcher.dispatch_notifications([notification.get('pid')]) flush_index(NotificationsSearch.Meta.index) flush_index(LoansSearch.Meta.index) assert number_of_reminders_sent(loan) == 1 diff --git a/tests/api/loans/test_loans_rest.py b/tests/api/loans/test_loans_rest.py index a913dd1f16..b27b794045 100644 --- a/tests/api/loans/test_loans_rest.py +++ b/tests/api/loans/test_loans_rest.py @@ -34,6 +34,7 @@ get_due_soon_loans, get_last_transaction_loc_for_item, get_overdue_loans from rero_ils.modules.notifications.api import Notification, \ NotificationsSearch, number_of_reminders_sent +from rero_ils.modules.notifications.dispatcher import Dispatcher def test_loans_permissions(client, loan_pending_martigny, json_header): @@ -203,8 +204,9 @@ def test_overdue_loans(client, librarian_martigny, assert overdue_loans[0].get('pid') == loan_pid assert number_of_reminders_sent(loan) == 0 - loan.create_notification( + notification = loan.create_notification( notification_type=Notification.OVERDUE_NOTIFICATION_TYPE) + Dispatcher.dispatch_notifications([notification.get('pid')]) flush_index(NotificationsSearch.Meta.index) flush_index(LoansSearch.Meta.index) assert number_of_reminders_sent(loan) == 1 diff --git a/tests/api/notifications/test_notifications_rest.py b/tests/api/notifications/test_notifications_rest.py index 9c1c8c0bb7..915f2e93e6 100644 --- a/tests/api/notifications/test_notifications_rest.py +++ b/tests/api/notifications/test_notifications_rest.py @@ -27,9 +27,11 @@ from utils import VerifyRecordPermissionPatch, flush_index, get_json, \ postdata, to_relative_url +from rero_ils.modules.libraries.api import email_notification_type from rero_ils.modules.loans.api import Loan, LoanAction from rero_ils.modules.notifications.api import Notification, \ NotificationsSearch, get_notification +from rero_ils.modules.notifications.tasks import process_notifications def test_notifications_permissions( @@ -360,8 +362,10 @@ def test_recall_notification(client, patron_sion, lib_sion, assert not get_notification( loan, notification_type=Notification.AVAILABILITY_NOTIFICATION_TYPE) + for notification_type in Notification.ALL_NOTIFICATIONS: + process_notifications(notification_type) # one new email for the patron - assert mailbox[0].recipients == [patron_sion.dumps()['email']] + assert mailbox[-1].recipients == [patron_sion.dumps()['email']] mailbox.clear() @@ -444,10 +448,6 @@ def test_recall_notification_without_email( ) ) assert res.status_code == 200 - - request_loan_pid = data.get( - 'action_applied')[LoanAction.REQUEST].get('pid') - flush_index(NotificationsSearch.Meta.index) assert loan.is_notified( @@ -460,10 +460,11 @@ def test_recall_notification_without_email( assert not get_notification( loan, notification_type=Notification.AVAILABILITY_NOTIFICATION_TYPE) + for notification_type in Notification.ALL_NOTIFICATIONS: + process_notifications(notification_type) # one new email for the librarian - assert mailbox[0].recipients == [ - lib_martigny.email_notification_type( - notification['notification_type'])] + assert mailbox[0].recipients == [email_notification_type( + lib_martigny, notification['notification_type'])] mailbox.clear() diff --git a/tests/api/selfcheck/test_selfcheck.py b/tests/api/selfcheck/test_selfcheck.py index 0c16274ff7..5f64e2a3cc 100644 --- a/tests/api/selfcheck/test_selfcheck.py +++ b/tests/api/selfcheck/test_selfcheck.py @@ -30,6 +30,7 @@ from rero_ils.modules.loans.api import Loan, LoanAction, LoanState from rero_ils.modules.notifications.api import Notification, \ NotificationsSearch, number_of_reminders_sent +from rero_ils.modules.notifications.dispatcher import Dispatcher from rero_ils.modules.selfcheck.api import authorize_patron, enable_patron, \ item_information, patron_information, selfcheck_checkin, \ selfcheck_checkout, selfcheck_login, system_status, \ @@ -143,8 +144,9 @@ def test_patron_information(client, librarian_martigny, ) loan = Loan.get_record_by_pid(loan_pid) assert loan.is_loan_overdue() - loan.create_notification( + notification = loan.create_notification( notification_type=Notification.OVERDUE_NOTIFICATION_TYPE) + Dispatcher.dispatch_notifications([notification.get('pid')]) flush_index(NotificationsSearch.Meta.index) flush_index(LoansSearch.Meta.index) assert number_of_reminders_sent(loan) == 1 @@ -214,8 +216,9 @@ def test_item_information(client, librarian_martigny, loan = Loan.get_record_by_pid(loan_pid) assert loan['state'] == LoanState.ITEM_ON_LOAN assert loan.is_loan_overdue() - loan.create_notification( + notification = loan.create_notification( notification_type=Notification.OVERDUE_NOTIFICATION_TYPE) + Dispatcher.dispatch_notifications([notification.get('pid')]) flush_index(NotificationsSearch.Meta.index) flush_index(LoansSearch.Meta.index) assert number_of_reminders_sent(loan) == 1 diff --git a/tests/api/test_tasks.py b/tests/api/test_tasks.py index 95603c1584..84f30278cd 100644 --- a/tests/api/test_tasks.py +++ b/tests/api/test_tasks.py @@ -99,7 +99,7 @@ def test_notifications_task( create_notifications(types=[ Notification.DUE_SOON_NOTIFICATION_TYPE, Notification.OVERDUE_NOTIFICATION_TYPE - ], process=False) + ]) flush_index(NotificationsSearch.Meta.index) flush_index(LoansSearch.Meta.index) assert loan.is_notified(Notification.OVERDUE_NOTIFICATION_TYPE, 0) @@ -112,7 +112,7 @@ def test_notifications_task( create_notifications(types=[ Notification.DUE_SOON_NOTIFICATION_TYPE, Notification.OVERDUE_NOTIFICATION_TYPE - ], tstamp=datetime.now(timezone.utc), process=False) + ], tstamp=datetime.now(timezone.utc)) assert number_of_reminders_sent( loan, notification_type=Notification.OVERDUE_NOTIFICATION_TYPE) == 1 @@ -129,7 +129,7 @@ def test_notifications_task( create_notifications(types=[ Notification.DUE_SOON_NOTIFICATION_TYPE, Notification.OVERDUE_NOTIFICATION_TYPE - ], process=False) + ]) flush_index(NotificationsSearch.Meta.index) flush_index(LoansSearch.Meta.index) assert loan.is_notified(Notification.OVERDUE_NOTIFICATION_TYPE, 1) diff --git a/tests/data/data.json b/tests/data/data.json index fb8864e650..ad78a999b5 100644 --- a/tests/data/data.json +++ b/tests/data/data.json @@ -144,6 +144,11 @@ { "type": "recall", "email": "reroilstest+martigny@gmail.com" + }, + { + "type": "availability", + "email": "reroilstest+martigny@gmail.com", + "delay": 0 } ] }, @@ -3876,4 +3881,4 @@ "home_phone": "+012024561414", "keep_history": true } -} +} \ No newline at end of file diff --git a/tests/fixtures/circulation.py b/tests/fixtures/circulation.py index dbb580ba28..ee6c88afc7 100644 --- a/tests/fixtures/circulation.py +++ b/tests/fixtures/circulation.py @@ -444,6 +444,55 @@ def loan_validated_martigny( return loan +@pytest.fixture(scope="module") +def loan2_validated_martigny( + app, + document, + item3_lib_martigny, + loc_public_martigny, + item_type_standard_martigny, + librarian_martigny, + patron_martigny, + circulation_policies): + """Request and validate item to a patron. + + item3_lib_martigny is requested and validated to patron_martigny. + """ + transaction_date = datetime.now(timezone.utc).isoformat() + + item3_lib_martigny.request( + patron_pid=patron_martigny.pid, + transaction_location_pid=loc_public_martigny.pid, + transaction_user_pid=librarian_martigny.pid, + transaction_date=transaction_date, + pickup_location_pid=loc_public_martigny.pid, + document_pid=extracted_data_from_ref( + item3_lib_martigny.get('document')) + ) + flush_index(ItemsSearch.Meta.index) + flush_index(LoansSearch.Meta.index) + flush_index(NotificationsSearch.Meta.index) + + loan = list(item3_lib_martigny.get_loans_by_item_pid( + item_pid=item3_lib_martigny.pid))[0] + item3_lib_martigny.validate_request( + pid=loan.pid, + patron_pid=patron_martigny.pid, + transaction_location_pid=loc_public_martigny.pid, + transaction_user_pid=librarian_martigny.pid, + transaction_date=transaction_date, + pickup_location_pid=loc_public_martigny.pid, + document_pid=extracted_data_from_ref( + item3_lib_martigny.get('document')) + ) + flush_index(ItemsSearch.Meta.index) + flush_index(LoansSearch.Meta.index) + flush_index(NotificationsSearch.Meta.index) + loan = list(item3_lib_martigny.get_loans_by_item_pid( + item_pid=item3_lib_martigny.pid))[0] + return loan + + @pytest.fixture(scope="module") def loan_validated_sion( app, @@ -498,6 +547,15 @@ def notification_availability_martigny(loan_validated_martigny): ) +@pytest.fixture(scope="module") +def notification2_availability_martigny(loan2_validated_martigny): + """Availability notification of martigny.""" + return get_notification( + loan2_validated_martigny, + notification_type=Notification.AVAILABILITY_NOTIFICATION_TYPE + ) + + @pytest.fixture(scope="module") def notification_availability_sion(loan_validated_sion): """Availability notification of sion.""" @@ -506,6 +564,14 @@ def notification_availability_sion(loan_validated_sion): notification_type=Notification.AVAILABILITY_NOTIFICATION_TYPE ) + +@pytest.fixture(scope="module") +def notification_availability_sion2(loan_validated_sion2): + """Availability notification of sion.""" + return get_notification( + loan_validated_sion2, + notification_type=Notification.AVAILABILITY_NOTIFICATION_TYPE + ) # ------------ Notifications: dummy notification ---------- diff --git a/tests/ui/notifications/test_notifications_api.py b/tests/ui/notifications/test_notifications_api.py index 50e523c2eb..d7b8967b10 100644 --- a/tests/ui/notifications/test_notifications_api.py +++ b/tests/ui/notifications/test_notifications_api.py @@ -23,6 +23,7 @@ from utils import get_mapping +from rero_ils.modules.libraries.api import email_notification_type from rero_ils.modules.notifications.api import Notification, \ NotificationsSearch from rero_ils.modules.notifications.dispatcher import Dispatcher @@ -37,11 +38,8 @@ def test_notification_es_mapping( assert mapping notif = deepcopy(dummy_notification) - notif_data = { - 'loan_url': 'https://ils.rero.ch/api/loans/', - 'pid': loan_validated_martigny.get('pid') - } - loan_ref = '{loan_url}{pid}'.format(**notif_data) + validated_pid = loan_validated_martigny.get('pid') + loan_ref = f'https://ils.rero.ch/api/loans/{validated_pid}' notif['loan'] = {"$ref": loan_ref} Notification.create(notif, dbcommit=True, delete_pid=True, reindex=True) @@ -60,75 +58,53 @@ def test_notification_organisation_pid( assert notification_availability_martigny.can_delete -def test_notification_mail( - notification_late_martigny, lib_martigny, mailbox): +def test_notification_mail(notification_late_martigny, lib_martigny, mailbox): """Test notification creation. Patron communication channel is mail. """ mailbox.clear() - notification_late_martigny.dispatch(enqueue=False, verbose=True) - assert mailbox[0].recipients == [ - lib_martigny.email_notification_type( - notification_late_martigny['notification_type'])] + Dispatcher.dispatch_notifications(notification_late_martigny['pid']) + assert mailbox[0].recipients == [email_notification_type( + lib_martigny, notification_late_martigny['notification_type'])] -def test_notification_email( - notification_late_sion, patron_sion, mailbox): +def test_notification_email(notification_late_sion, patron_sion, mailbox): """Test overdue notification. Patron communication channel is email. """ mailbox.clear() - notification_late_sion.dispatch(enqueue=False, verbose=True) + Dispatcher.dispatch_notifications(notification_late_sion['pid']) assert mailbox[0].recipients == [patron_sion.dumps()['email']] -def test_notification_email_availability( - notification_availability_sion, lib_sion, patron_sion, mailbox): +def test_notification_email_availability(notification_availability_sion, + lib_sion, patron_sion, mailbox): """Test availibility notification. Patron communication channel is email. """ mailbox.clear() - notification_availability_sion.dispatch(enqueue=False, verbose=True) + Dispatcher.dispatch_notifications(notification_availability_sion['pid']) assert mailbox[0].recipients == [patron_sion.dumps()['email']] -def test_notification_dispatch(app): - """Test notification dispatch.""" - - class DummyNotification(object): - - data = { - 'pid': 'dummy_notification_pid', - 'notification_type': 'dummy_notification' - } - - def __init__(self, communication_channel): - self.communication_channel = communication_channel - - def __getitem__(self, key): - return self.data[key] - - def get(self, key): - return self.__getitem__(key) - - def init_loan(self): - return None - - def replace_pids_and_refs(self): - return { - 'loan': { - 'pid': 'dummy_notification_loan_pid', - 'patron': { - 'pid': 'dummy_patron_pid', - 'patron': { - 'communication_channel': self.communication_channel - } - } - } - } - - def update_process_date(self): - return self - - notification = DummyNotification('XXXX') - Dispatcher().dispatch_notification(notification=notification, verbose=True) +def test_notification_email_aggregated(notification_availability_martigny, + notification2_availability_martigny, + lib_martigny, patron_martigny, mailbox): + """Test availibility notification. + Patron communication channel is email. + """ + mailbox.clear() + Dispatcher.dispatch_notifications([ + notification_availability_martigny['pid'], + notification2_availability_martigny['pid'] + ], verbose=True) + assert len(mailbox) == 1 + from pprint import pprint + pprint(mailbox[0]) + + recipient = '???' + for notification_setting in lib_martigny.get('notification_settings'): + if notification_setting['type'] == \ + Notification.AVAILABILITY_NOTIFICATION_TYPE: + recipient = notification_setting['email'] + assert mailbox[0].recipients == [recipient]