Skip to content

Commit

Permalink
notifications: log the error
Browse files Browse the repository at this point in the history
* Intercepts an exception and log it for sentry.
* Fixes monitoring JSON export by replacing `-` in the object keys.

Co-Authored-by: Johnny Mariéthoz <Johnny.Mariethoz@rero.ch>
  • Loading branch information
jma authored and iGor milhit committed Aug 26, 2021
1 parent eb7a9b6 commit 38eb633
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 150 deletions.
2 changes: 2 additions & 0 deletions rero_ils/modules/monitoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ def timestamps():
time_stamps = current_cache.get('timestamps')
if time_stamps:
for name, values in time_stamps.items():
# make the name safe for JSON export
name = name.replace('-', '_')
data[name] = {}
for key, value in values.items():
if key == 'time':
Expand Down
319 changes: 169 additions & 150 deletions rero_ils/modules/notifications/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@
from invenio_mail.tasks import send_email as task_send_email
from num2words import num2words

from .api import Notification
from .models import NotificationChannel, NotificationType
from .utils import get_communication_channel_to_use, get_template_to_use
from ..libraries.api import email_notification_type
from ..locations.api import Location
from ..patrons.api import Patron
from ...filter import format_date_filter
from ...utils import language_iso639_2to1

Expand All @@ -45,10 +47,6 @@ def dispatch_notifications(cls, notification_pids=None, resend=False,
:param verbose: Verbose output.
:returns: dictionary with processed and send count
"""
from .api import Notification
from ..items.api import Item
from ..loans.api import LoanState
from ..patrons.api import Patron

def get_dispatcher_function(channel):
try:
Expand All @@ -74,152 +72,14 @@ def get_dispatcher_function(channel):
# 3. library
# 4. patron
for notification in notifications:
n_type = notification['notification_type']
process_date = notification.get('process_date')

# 1. Check if notification has already been processed and if we
# need to resend it. If not, skip this notification and continue
if process_date:
current_app.logger.warning(
f'Notification: {notification.pid} already processed '
f'on: {process_date}'
)
if not resend:
continue

data = notification.replace_pids_and_refs()
loan = data['loan']
patron = loan['patron']

# 2. Find the communication channel to use to dispatch this
# notification. The channel depends on the loan, the
# notification type and the related patron
communication_channel = get_communication_channel_to_use(
loan,
notification,
patron
)

# 3. Get the communication language to use. Except for internal
# notification, the language to use is defined into the related
# patron account. For Internal notifications, the language is
# the library defined language.
communication_lang = patron['patron']['communication_language']
if n_type in NotificationType.INTERNAL_NOTIFICATIONS:
communication_lang = loan['library']['communication_language']
language = language_iso639_2to1(communication_lang)

# 4. Compute the reminder counter.
# For some notification (overdue, ...) the reminder counter is
# an information to use into the message to send. We need to
# translate this counter into a localized string.
reminder_counter = data.get('reminder_counter', 0)
reminder = reminder_counter + 1
reminder = num2words(reminder, to='ordinal', lang=language)

# 5. Get the template to use for the notification.
# Depending of the notification and the reminder counter, find
# the correct template file to use.
tpl_path = get_template_to_use(
loan, n_type, reminder_counter).rstrip('/')
template = f'{tpl_path}/{communication_lang}.txt'

# 6. Build the context to use to render the template.
ctx_data = {
'notification_type': n_type,
'creation_date': format_date_filter(
notification.get('creation_date'),
date_format='medium',
locale=language
),
'in_transit': loan['state'] in [
LoanState.ITEM_IN_TRANSIT_TO_HOUSE,
LoanState.ITEM_IN_TRANSIT_FOR_PICKUP
],
'template': template,
'profile_url': loan['profile_url'],
'patron': patron,
'library': loan['library'],
'pickup_library': loan['pickup_library'],
'transaction_library': loan['transaction_library'],
'pickup_name': loan['pickup_name'],
'documents': [],
'notifications': []
}
transaction_location = loan.get('transaction_location')
if transaction_location:
ctx_data['transaction_location_name'] = \
transaction_location['name']

# aggregate notifications
l_pid = loan['library']['pid']
p_pid = patron['pid']
c_channel = communication_channel

aggregated.setdefault(n_type, {})
aggregated[n_type].setdefault(c_channel, {})
aggregated[n_type][c_channel].setdefault(l_pid, {})
aggregated[n_type][c_channel][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')
}
documents_data = {k: v for k, v in documents_data.items() if v}

# Add item to document
item_data = loan.get('item')
if item_data:
if n_type in [
NotificationType.BOOKING,
NotificationType.AVAILABILITY
]:
# get item from the checkin loan
item = Item.get_record_by_pid(item_data.get('pid'))
# get the requested loan it can be in several states
# due to the automatic request validation
request_loan = None
for state in [
LoanState.ITEM_AT_DESK,
LoanState.ITEM_IN_TRANSIT_FOR_PICKUP,
LoanState.PENDING
]:
request_loan = item.get_first_loan_by_state(state)
if request_loan:
break
request_patron = Patron.get_record_by_pid(
request_loan['patron_pid'])
ctx_data['request_patron'] = \
request_patron.replace_refs().dumps()
pickup_location = Location.get_record_by_pid(
request_loan['pickup_location_pid'])
ctx_data['request_pickup_name'] = \
pickup_location['pickup_name']

documents_data['item'] = {
'barcode': item_data['barcode'],
'call_number': item_data.get('call_number'),
'second_call_number': item_data.get('second_call_number')
}
documents_data['item'] = \
{k: v for k, v in documents_data['item'].items() if v}
location = item_data.get('location')
if location:
loc = Location.get_record_by_pid(location.get('pid'))
lib = loc.get_library()
documents_data['item']['location_name'] = loc.get('name')
documents_data['item']['library_name'] = lib.get('name')
email = loc.get('notification_email')
if email:
ctx_data['location_email'] = email

# Add information into correct aggregations
aggregation = aggregated[n_type][c_channel][l_pid][p_pid]
aggregation['documents'].append(documents_data)
aggregation['notifications'].append(notification)
try:
cls._process_notification(
notification, resend, aggregated)
except Exception as error:
current_app.logger.error(
f'Notification has not be sent (pid: {notification.pid},'
f' type: {notification["notification_type"]}): '
f'{error}')

# SEND AGGREGATED NOTIFICATIONS
for notification_type, notification_values in aggregated.items():
Expand Down Expand Up @@ -248,6 +108,165 @@ def get_dispatcher_function(channel):
'not_sent': not_sent
}

@classmethod
def _process_notification(cls, notification, resend, aggregated):
"""Process one notification.
:param notification: Notification to process.
:param resend: Resend notification if already send.
:param aggregated: Dict to store notification results.
:returns: A dispatcher function.
"""
from ..items.api import Item
from ..loans.api import LoanState

n_type = notification['notification_type']
process_date = notification.get('process_date')

# 1. Check if notification has already been processed and if we
# need to resend it. If not, skip this notification and continue
if process_date:
current_app.logger.warning(
f'Notification: {notification.pid} already processed '
f'on: {process_date}'
)
if not resend:
return

data = notification.replace_pids_and_refs()
loan = data['loan']
patron = loan['patron']

# 2. Find the communication channel to use to dispatch this
# notification. The channel depends on the loan, the
# notification type and the related patron
communication_channel = get_communication_channel_to_use(
loan,
notification,
patron
)

# 3. Get the communication language to use. Except for internal
# notification, the language to use is defined into the related
# patron account. For Internal notifications, the language is
# the library defined language.
communication_lang = patron['patron']['communication_language']
if n_type in NotificationType.INTERNAL_NOTIFICATIONS:
communication_lang = loan['library']['communication_language']
language = language_iso639_2to1(communication_lang)

# 4. Compute the reminder counter.
# For some notification (overdue, ...) the reminder counter is
# an information to use into the message to send. We need to
# translate this counter into a localized string.
reminder_counter = data.get('reminder_counter', 0)
reminder = reminder_counter + 1
reminder = num2words(reminder, to='ordinal', lang=language)

# 5. Get the template to use for the notification.
# Depending of the notification and the reminder counter, find
# the correct template file to use.
tpl_path = get_template_to_use(
loan, n_type, reminder_counter).rstrip('/')
template = f'{tpl_path}/{communication_lang}.txt'

# 6. Build the context to use to render the template.
ctx_data = {
'notification_type': n_type,
'creation_date': format_date_filter(
notification.get('creation_date'),
date_format='medium',
locale=language
),
'in_transit': loan['state'] in [
LoanState.ITEM_IN_TRANSIT_TO_HOUSE,
LoanState.ITEM_IN_TRANSIT_FOR_PICKUP
],
'template': template,
'profile_url': loan['profile_url'],
'patron': patron,
'library': loan['library'],
'pickup_library': loan['pickup_library'],
'transaction_library': loan['transaction_library'],
'pickup_name': loan['pickup_name'],
'documents': [],
'notifications': []
}
transaction_location = loan.get('transaction_location')
if transaction_location:
ctx_data['transaction_location_name'] = \
transaction_location['name']

# aggregate notifications
l_pid = loan['library']['pid']
p_pid = patron['pid']
c_channel = communication_channel

aggregated.setdefault(n_type, {})
aggregated[n_type].setdefault(c_channel, {})
aggregated[n_type][c_channel].setdefault(l_pid, {})
aggregated[n_type][c_channel][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')
}
documents_data = {k: v for k, v in documents_data.items() if v}

# Add item to document
item_data = loan.get('item')
if item_data:
if n_type in [
NotificationType.BOOKING,
NotificationType.AVAILABILITY
]:
# get item from the checkin loan
item = Item.get_record_by_pid(item_data.get('pid'))
# get the requested loan it can be in several states
# due to the automatic request validation
request_loan = None
for state in [
LoanState.ITEM_AT_DESK,
LoanState.ITEM_IN_TRANSIT_FOR_PICKUP,
LoanState.PENDING
]:
request_loan = item.get_first_loan_by_state(state)
if request_loan:
break
request_patron = Patron.get_record_by_pid(
request_loan['patron_pid'])
ctx_data['request_patron'] = \
request_patron.replace_refs().dumps()
pickup_location = Location.get_record_by_pid(
request_loan['pickup_location_pid'])
ctx_data['request_pickup_name'] = \
pickup_location['pickup_name']

documents_data['item'] = {
'barcode': item_data['barcode'],
'call_number': item_data.get('call_number'),
'second_call_number': item_data.get('second_call_number')
}
documents_data['item'] = \
{k: v for k, v in documents_data['item'].items() if v}
location = item_data.get('location')
if location:
loc = Location.get_record_by_pid(location.get('pid'))
lib = loc.get_library()
documents_data['item']['location_name'] = loc.get('name')
documents_data['item']['library_name'] = lib.get('name')
email = loc.get('notification_email')
if email:
ctx_data['location_email'] = email

# Add information into correct aggregations
aggregation = aggregated[n_type][c_channel][l_pid][p_pid]
aggregation['documents'].append(documents_data)
aggregation['notifications'].append(notification)

@staticmethod
def _create_email(recipients, reply_to, ctx_data, template):
"""Create email message from template.
Expand Down
Loading

0 comments on commit 38eb633

Please sign in to comment.