Skip to content

Commit 38eb633

Browse files
jmaiGor milhit
authored and
iGor milhit
committed
notifications: log the error
* Intercepts an exception and log it for sentry. * Fixes monitoring JSON export by replacing `-` in the object keys. Co-Authored-by: Johnny Mariéthoz <[email protected]>
1 parent eb7a9b6 commit 38eb633

File tree

3 files changed

+218
-150
lines changed

3 files changed

+218
-150
lines changed

rero_ils/modules/monitoring.py

+2
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,8 @@ def timestamps():
308308
time_stamps = current_cache.get('timestamps')
309309
if time_stamps:
310310
for name, values in time_stamps.items():
311+
# make the name safe for JSON export
312+
name = name.replace('-', '_')
311313
data[name] = {}
312314
for key, value in values.items():
313315
if key == 'time':

rero_ils/modules/notifications/dispatcher.py

+169-150
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424
from invenio_mail.tasks import send_email as task_send_email
2525
from num2words import num2words
2626

27+
from .api import Notification
2728
from .models import NotificationChannel, NotificationType
2829
from .utils import get_communication_channel_to_use, get_template_to_use
2930
from ..libraries.api import email_notification_type
3031
from ..locations.api import Location
32+
from ..patrons.api import Patron
3133
from ...filter import format_date_filter
3234
from ...utils import language_iso639_2to1
3335

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

5351
def get_dispatcher_function(channel):
5452
try:
@@ -74,152 +72,14 @@ def get_dispatcher_function(channel):
7472
# 3. library
7573
# 4. patron
7674
for notification in notifications:
77-
n_type = notification['notification_type']
78-
process_date = notification.get('process_date')
79-
80-
# 1. Check if notification has already been processed and if we
81-
# need to resend it. If not, skip this notification and continue
82-
if process_date:
83-
current_app.logger.warning(
84-
f'Notification: {notification.pid} already processed '
85-
f'on: {process_date}'
86-
)
87-
if not resend:
88-
continue
89-
90-
data = notification.replace_pids_and_refs()
91-
loan = data['loan']
92-
patron = loan['patron']
93-
94-
# 2. Find the communication channel to use to dispatch this
95-
# notification. The channel depends on the loan, the
96-
# notification type and the related patron
97-
communication_channel = get_communication_channel_to_use(
98-
loan,
99-
notification,
100-
patron
101-
)
102-
103-
# 3. Get the communication language to use. Except for internal
104-
# notification, the language to use is defined into the related
105-
# patron account. For Internal notifications, the language is
106-
# the library defined language.
107-
communication_lang = patron['patron']['communication_language']
108-
if n_type in NotificationType.INTERNAL_NOTIFICATIONS:
109-
communication_lang = loan['library']['communication_language']
110-
language = language_iso639_2to1(communication_lang)
111-
112-
# 4. Compute the reminder counter.
113-
# For some notification (overdue, ...) the reminder counter is
114-
# an information to use into the message to send. We need to
115-
# translate this counter into a localized string.
116-
reminder_counter = data.get('reminder_counter', 0)
117-
reminder = reminder_counter + 1
118-
reminder = num2words(reminder, to='ordinal', lang=language)
119-
120-
# 5. Get the template to use for the notification.
121-
# Depending of the notification and the reminder counter, find
122-
# the correct template file to use.
123-
tpl_path = get_template_to_use(
124-
loan, n_type, reminder_counter).rstrip('/')
125-
template = f'{tpl_path}/{communication_lang}.txt'
126-
127-
# 6. Build the context to use to render the template.
128-
ctx_data = {
129-
'notification_type': n_type,
130-
'creation_date': format_date_filter(
131-
notification.get('creation_date'),
132-
date_format='medium',
133-
locale=language
134-
),
135-
'in_transit': loan['state'] in [
136-
LoanState.ITEM_IN_TRANSIT_TO_HOUSE,
137-
LoanState.ITEM_IN_TRANSIT_FOR_PICKUP
138-
],
139-
'template': template,
140-
'profile_url': loan['profile_url'],
141-
'patron': patron,
142-
'library': loan['library'],
143-
'pickup_library': loan['pickup_library'],
144-
'transaction_library': loan['transaction_library'],
145-
'pickup_name': loan['pickup_name'],
146-
'documents': [],
147-
'notifications': []
148-
}
149-
transaction_location = loan.get('transaction_location')
150-
if transaction_location:
151-
ctx_data['transaction_location_name'] = \
152-
transaction_location['name']
153-
154-
# aggregate notifications
155-
l_pid = loan['library']['pid']
156-
p_pid = patron['pid']
157-
c_channel = communication_channel
158-
159-
aggregated.setdefault(n_type, {})
160-
aggregated[n_type].setdefault(c_channel, {})
161-
aggregated[n_type][c_channel].setdefault(l_pid, {})
162-
aggregated[n_type][c_channel][l_pid].setdefault(p_pid, ctx_data)
163-
164-
documents_data = {
165-
'title_text': loan['document']['title_text'],
166-
'responsibility_statement':
167-
loan['document']['responsibility_statement'],
168-
'reminder': reminder,
169-
'end_date': loan.get('end_date')
170-
}
171-
documents_data = {k: v for k, v in documents_data.items() if v}
172-
173-
# Add item to document
174-
item_data = loan.get('item')
175-
if item_data:
176-
if n_type in [
177-
NotificationType.BOOKING,
178-
NotificationType.AVAILABILITY
179-
]:
180-
# get item from the checkin loan
181-
item = Item.get_record_by_pid(item_data.get('pid'))
182-
# get the requested loan it can be in several states
183-
# due to the automatic request validation
184-
request_loan = None
185-
for state in [
186-
LoanState.ITEM_AT_DESK,
187-
LoanState.ITEM_IN_TRANSIT_FOR_PICKUP,
188-
LoanState.PENDING
189-
]:
190-
request_loan = item.get_first_loan_by_state(state)
191-
if request_loan:
192-
break
193-
request_patron = Patron.get_record_by_pid(
194-
request_loan['patron_pid'])
195-
ctx_data['request_patron'] = \
196-
request_patron.replace_refs().dumps()
197-
pickup_location = Location.get_record_by_pid(
198-
request_loan['pickup_location_pid'])
199-
ctx_data['request_pickup_name'] = \
200-
pickup_location['pickup_name']
201-
202-
documents_data['item'] = {
203-
'barcode': item_data['barcode'],
204-
'call_number': item_data.get('call_number'),
205-
'second_call_number': item_data.get('second_call_number')
206-
}
207-
documents_data['item'] = \
208-
{k: v for k, v in documents_data['item'].items() if v}
209-
location = item_data.get('location')
210-
if location:
211-
loc = Location.get_record_by_pid(location.get('pid'))
212-
lib = loc.get_library()
213-
documents_data['item']['location_name'] = loc.get('name')
214-
documents_data['item']['library_name'] = lib.get('name')
215-
email = loc.get('notification_email')
216-
if email:
217-
ctx_data['location_email'] = email
218-
219-
# Add information into correct aggregations
220-
aggregation = aggregated[n_type][c_channel][l_pid][p_pid]
221-
aggregation['documents'].append(documents_data)
222-
aggregation['notifications'].append(notification)
75+
try:
76+
cls._process_notification(
77+
notification, resend, aggregated)
78+
except Exception as error:
79+
current_app.logger.error(
80+
f'Notification has not be sent (pid: {notification.pid},'
81+
f' type: {notification["notification_type"]}): '
82+
f'{error}')
22383

22484
# SEND AGGREGATED NOTIFICATIONS
22585
for notification_type, notification_values in aggregated.items():
@@ -248,6 +108,165 @@ def get_dispatcher_function(channel):
248108
'not_sent': not_sent
249109
}
250110

111+
@classmethod
112+
def _process_notification(cls, notification, resend, aggregated):
113+
"""Process one notification.
114+
115+
:param notification: Notification to process.
116+
:param resend: Resend notification if already send.
117+
:param aggregated: Dict to store notification results.
118+
:returns: A dispatcher function.
119+
"""
120+
from ..items.api import Item
121+
from ..loans.api import LoanState
122+
123+
n_type = notification['notification_type']
124+
process_date = notification.get('process_date')
125+
126+
# 1. Check if notification has already been processed and if we
127+
# need to resend it. If not, skip this notification and continue
128+
if process_date:
129+
current_app.logger.warning(
130+
f'Notification: {notification.pid} already processed '
131+
f'on: {process_date}'
132+
)
133+
if not resend:
134+
return
135+
136+
data = notification.replace_pids_and_refs()
137+
loan = data['loan']
138+
patron = loan['patron']
139+
140+
# 2. Find the communication channel to use to dispatch this
141+
# notification. The channel depends on the loan, the
142+
# notification type and the related patron
143+
communication_channel = get_communication_channel_to_use(
144+
loan,
145+
notification,
146+
patron
147+
)
148+
149+
# 3. Get the communication language to use. Except for internal
150+
# notification, the language to use is defined into the related
151+
# patron account. For Internal notifications, the language is
152+
# the library defined language.
153+
communication_lang = patron['patron']['communication_language']
154+
if n_type in NotificationType.INTERNAL_NOTIFICATIONS:
155+
communication_lang = loan['library']['communication_language']
156+
language = language_iso639_2to1(communication_lang)
157+
158+
# 4. Compute the reminder counter.
159+
# For some notification (overdue, ...) the reminder counter is
160+
# an information to use into the message to send. We need to
161+
# translate this counter into a localized string.
162+
reminder_counter = data.get('reminder_counter', 0)
163+
reminder = reminder_counter + 1
164+
reminder = num2words(reminder, to='ordinal', lang=language)
165+
166+
# 5. Get the template to use for the notification.
167+
# Depending of the notification and the reminder counter, find
168+
# the correct template file to use.
169+
tpl_path = get_template_to_use(
170+
loan, n_type, reminder_counter).rstrip('/')
171+
template = f'{tpl_path}/{communication_lang}.txt'
172+
173+
# 6. Build the context to use to render the template.
174+
ctx_data = {
175+
'notification_type': n_type,
176+
'creation_date': format_date_filter(
177+
notification.get('creation_date'),
178+
date_format='medium',
179+
locale=language
180+
),
181+
'in_transit': loan['state'] in [
182+
LoanState.ITEM_IN_TRANSIT_TO_HOUSE,
183+
LoanState.ITEM_IN_TRANSIT_FOR_PICKUP
184+
],
185+
'template': template,
186+
'profile_url': loan['profile_url'],
187+
'patron': patron,
188+
'library': loan['library'],
189+
'pickup_library': loan['pickup_library'],
190+
'transaction_library': loan['transaction_library'],
191+
'pickup_name': loan['pickup_name'],
192+
'documents': [],
193+
'notifications': []
194+
}
195+
transaction_location = loan.get('transaction_location')
196+
if transaction_location:
197+
ctx_data['transaction_location_name'] = \
198+
transaction_location['name']
199+
200+
# aggregate notifications
201+
l_pid = loan['library']['pid']
202+
p_pid = patron['pid']
203+
c_channel = communication_channel
204+
205+
aggregated.setdefault(n_type, {})
206+
aggregated[n_type].setdefault(c_channel, {})
207+
aggregated[n_type][c_channel].setdefault(l_pid, {})
208+
aggregated[n_type][c_channel][l_pid].setdefault(p_pid, ctx_data)
209+
210+
documents_data = {
211+
'title_text': loan['document']['title_text'],
212+
'responsibility_statement':
213+
loan['document']['responsibility_statement'],
214+
'reminder': reminder,
215+
'end_date': loan.get('end_date')
216+
}
217+
documents_data = {k: v for k, v in documents_data.items() if v}
218+
219+
# Add item to document
220+
item_data = loan.get('item')
221+
if item_data:
222+
if n_type in [
223+
NotificationType.BOOKING,
224+
NotificationType.AVAILABILITY
225+
]:
226+
# get item from the checkin loan
227+
item = Item.get_record_by_pid(item_data.get('pid'))
228+
# get the requested loan it can be in several states
229+
# due to the automatic request validation
230+
request_loan = None
231+
for state in [
232+
LoanState.ITEM_AT_DESK,
233+
LoanState.ITEM_IN_TRANSIT_FOR_PICKUP,
234+
LoanState.PENDING
235+
]:
236+
request_loan = item.get_first_loan_by_state(state)
237+
if request_loan:
238+
break
239+
request_patron = Patron.get_record_by_pid(
240+
request_loan['patron_pid'])
241+
ctx_data['request_patron'] = \
242+
request_patron.replace_refs().dumps()
243+
pickup_location = Location.get_record_by_pid(
244+
request_loan['pickup_location_pid'])
245+
ctx_data['request_pickup_name'] = \
246+
pickup_location['pickup_name']
247+
248+
documents_data['item'] = {
249+
'barcode': item_data['barcode'],
250+
'call_number': item_data.get('call_number'),
251+
'second_call_number': item_data.get('second_call_number')
252+
}
253+
documents_data['item'] = \
254+
{k: v for k, v in documents_data['item'].items() if v}
255+
location = item_data.get('location')
256+
if location:
257+
loc = Location.get_record_by_pid(location.get('pid'))
258+
lib = loc.get_library()
259+
documents_data['item']['location_name'] = loc.get('name')
260+
documents_data['item']['library_name'] = lib.get('name')
261+
email = loc.get('notification_email')
262+
if email:
263+
ctx_data['location_email'] = email
264+
265+
# Add information into correct aggregations
266+
aggregation = aggregated[n_type][c_channel][l_pid][p_pid]
267+
aggregation['documents'].append(documents_data)
268+
aggregation['notifications'].append(notification)
269+
251270
@staticmethod
252271
def _create_email(recipients, reply_to, ctx_data, template):
253272
"""Create email message from template.

0 commit comments

Comments
 (0)