Skip to content

Commit

Permalink
items: claim notification serialization
Browse files Browse the repository at this point in the history
* Adds claim notification information into ES data (for facetting)
* Adds claim notification information for item `rero+json` serializer.
* Forces `communication_language` field of `Vendor` resource as required
  field.
* Add management of "cc" and "bcc" emails by notification dispatcher.

Co-Authored-by: Renaud Michotte <renaud.michotte@gmail.com>
  • Loading branch information
zannkukai authored and PascalRepond committed Jun 16, 2023
1 parent 5fcef9e commit 1f33e19
Show file tree
Hide file tree
Showing 19 changed files with 161 additions and 90 deletions.
3 changes: 3 additions & 0 deletions data/vendors.json
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@
{
"pid": "13",
"name": "Tomes and Scrolls",
"communication_language": "ger",
"contacts": [
{
"type": "default",
Expand All @@ -326,6 +327,7 @@
{
"pid": "14",
"name": "Tomes and Scrolls",
"communication_language": "ger",
"contacts": [
{
"type": "default",
Expand All @@ -345,6 +347,7 @@
{
"pid": "15",
"name": "Tomes and Scrolls",
"communication_language": "ger",
"contacts": [
{
"type": "order",
Expand Down
64 changes: 15 additions & 49 deletions rero_ils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1745,55 +1745,21 @@ def _(x):
items=dict(
aggs=dict(
document_type=dict(
terms=dict(field='document.document_type.main_type',
size=DOCUMENTS_AGGREGATION_SIZE),
terms=dict(field='document.document_type.main_type', size=DOCUMENTS_AGGREGATION_SIZE),
aggs=dict(
document_subtype=dict(
terms=dict(field='document.document_type.subtype',
size=DOCUMENTS_AGGREGATION_SIZE)
)
document_subtype=dict(terms=dict(field='document.document_type.subtype', size=DOCUMENTS_AGGREGATION_SIZE))
)
),
library=dict(
terms=dict(
field='library.pid',
size=RERO_ILS_DEFAULT_AGGREGATION_SIZE)
),
location=dict(
terms=dict(
field='location.pid',
size=RERO_ILS_DEFAULT_AGGREGATION_SIZE)
),
item_type=dict(
terms=dict(
field='item_type.pid',
size=RERO_ILS_DEFAULT_AGGREGATION_SIZE)
),
temporary_location=dict(
terms=dict(
field='temporary_location.pid',
size=RERO_ILS_DEFAULT_AGGREGATION_SIZE)
),
temporary_item_type=dict(
terms=dict(
field='temporary_item_type.pid',
size=RERO_ILS_DEFAULT_AGGREGATION_SIZE)
),
status=dict(
terms=dict(
field='status',
size=RERO_ILS_DEFAULT_AGGREGATION_SIZE)
),
vendor=dict(
terms=dict(
field='vendor.pid',
size=RERO_ILS_DEFAULT_AGGREGATION_SIZE)
),
current_requests=dict(
max=dict(
field='current_pending_requests'
)
)
library=dict(terms=dict(field='library.pid', size=RERO_ILS_DEFAULT_AGGREGATION_SIZE)),
location=dict(terms=dict(field='location.pid', size=RERO_ILS_DEFAULT_AGGREGATION_SIZE)),
item_type=dict(terms=dict(field='item_type.pid', size=RERO_ILS_DEFAULT_AGGREGATION_SIZE)),
temporary_location=dict(terms=dict(field='temporary_location.pid', size=RERO_ILS_DEFAULT_AGGREGATION_SIZE)),
temporary_item_type=dict(terms=dict(field='temporary_item_type.pid', size=RERO_ILS_DEFAULT_AGGREGATION_SIZE)),
status=dict(terms=dict(field='status', size=RERO_ILS_DEFAULT_AGGREGATION_SIZE)),
vendor=dict(terms=dict(field='vendor.pid', size=RERO_ILS_DEFAULT_AGGREGATION_SIZE)),
claims_count=dict(terms=dict(field='issue.claims.counter', size=RERO_ILS_DEFAULT_AGGREGATION_SIZE)),
claims_date=dict(date_histogram=dict(field='issue.claims.dates', calendar_interval='1d', format='yyyy-MM-dd')),
current_requests=dict(max=dict(field='current_pending_requests'))
),
filters={
_('document_type'): and_term_filter('document.document_type.main_type'),
Expand All @@ -1805,9 +1771,9 @@ def _(x):
_('temporary_location'): and_term_filter('temporary_location.pid'),
_('status'): and_term_filter('status'),
_('vendor'): and_term_filter('vendor.pid'),
# to allow multiple filters support, in this case to filter by
# "late or claimed"
'or_issue_status': terms_filter('issue.status'),
_('or_issue_status'): terms_filter('issue.status'), # to allow multiple filters support, in this case to filter by "late or claimed"
_('claims_count'): and_term_filter('issue.claims.counter'),
_('claims_date'): range_filter('issue.claims.dates', format='epoch_millis', start_date_math='/d', end_date_math='/d'),
_('current_requests'): range_filter('current_pending_requests')
}
),
Expand Down
7 changes: 4 additions & 3 deletions rero_ils/modules/acquisition/acq_orders/api.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
#
# RERO ILS
# Copyright (C) 2019-2022 RERO
# Copyright (C) 2019-2022 UCLouvain
# Copyright (C) 2019-2023 RERO
# Copyright (C) 2019-2023 UCLouvain
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
Expand All @@ -17,7 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""API for manipulating Acquisition Orders."""
from datetime import datetime
from datetime import datetime, timezone
from functools import partial

from flask_babelex import gettext as _
Expand Down Expand Up @@ -456,6 +456,7 @@ def send_order(self, emails=None):
"""
# Create the notification and dispatch it synchronously.
record = {
'creation_date': datetime.now(timezone.utc).isoformat(),
'notification_type': NotificationType.ACQUISITION_ORDER,
'context': {
'order': {'$ref': get_ref_for_pid('acor', self.pid)},
Expand Down
9 changes: 8 additions & 1 deletion rero_ils/modules/items/api/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""API for manipulating the item issue."""
from datetime import datetime, timezone

from rero_ils.modules.notifications.api import Notification, \
NotificationsSearch
Expand Down Expand Up @@ -96,7 +97,12 @@ def vendor_pid(self):
@property
def claims_count(self):
"""Get the number of claims notification sent about this issue."""
return len(list(NotificationsSearch().get_claims(self.pid)))
return NotificationsSearch().get_claims_count(self.pid)

@property
def claim_notifications(self):
"""Get the `CLAIM_ISSUE` notifications related to this issue."""
return list(NotificationsSearch().get_claims(self.pid))

@property
def issue_inherited_first_call_number(self):
Expand Down Expand Up @@ -156,6 +162,7 @@ def claims(self, recipients):
"""
# Create the notification and dispatch it synchronously.
record = {
'creation_date': datetime.now(timezone.utc).isoformat(),
'notification_type': NotificationType.CLAIM_ISSUE,
'context': {
'item': {'$ref': get_ref_for_pid('item', self.pid)},
Expand Down
2 changes: 1 addition & 1 deletion rero_ils/modules/items/dumpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,4 @@ def dump(self, record, data):
'claim_counter': record.claims_count
})

return {k: v for k, v in data.items() if v}
return {k: v for k, v in data.items() if v is not None}
11 changes: 11 additions & 0 deletions rero_ils/modules/items/listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,14 @@ def enrich_item_data(sender, json=None, record=None, index=None,
# inject vendor pid
if vendor_pid := record.vendor_pid:
json['vendor'] = {'pid': vendor_pid, 'type': 'vndr'}
# inject claims information: counter and dates
if notifications := record.claim_notifications:
dates = [
notification['creation_date']
for notification in notifications
if 'creation_date' in notification
]
json['issue']['claims'] = {
'counter': len(notifications),
'dates': dates
}
11 changes: 11 additions & 0 deletions rero_ils/modules/items/mappings/v7/items/item-v0.0.1.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,17 @@
},
"status_date": {
"type": "date"
},
"claims": {
"type": "object",
"properties": {
"counter": {
"type": "integer"
},
"dates": {
"type": "date"
}
}
}
}
},
Expand Down
4 changes: 2 additions & 2 deletions rero_ils/modules/items/serializers/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,8 @@ def append_issue_data(hit, csv_data):
csv_data['issue_status_date'] = \
ciso8601.parse_datetime(
issue.get('status_date')).date()
csv_data['issue_claims_count'] = \
len(list(NotificationsSearch().get_claims(csv_data['item_pid'])))
csv_data['issue_claims_count'] = NotificationsSearch()\
.get_claims_count(csv_data['item_pid'])
csv_data['issue_expected_date'] = \
issue.get('expected_date')
csv_data['issue_regular'] = issue.get('regular')
Expand Down
23 changes: 23 additions & 0 deletions rero_ils/modules/items/serializers/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,27 @@ def _postprocess_search_aggregations(self, aggregations: dict) -> None:
'max': 100,
'step': 1
}
if aggr := aggregations.get('claims_date'):
JSONSerializer.add_date_range_configuration(aggr)

super()._postprocess_search_aggregations(aggregations)

def preprocess_record(self, pid, record, links_factory=None, **kwargs):
"""Prepare a record and persistent identifier for serialization.
:param pid: Persistent identifier instance.
:param record: Record instance.
:param links_factory: Factory function for record links.
"""
if record.is_issue and (notifications := record.claim_notifications):
dates = [
notification['creation_date']
for notification in notifications
if 'creation_date' in notification
]
record.setdefault('issue', {})['claims'] = {
'counter': len(notifications),
'dates': dates
}
return super().preprocess_record(
pid=pid, record=record, links_factory=links_factory, kwargs=kwargs)
4 changes: 4 additions & 0 deletions rero_ils/modules/items/views/api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,10 @@ def claim_notification_preview(item_pid):
abort(400, 'Item isn\'t an issue')

issue_data = record.dumps(dumper=ClaimIssueNotificationDumper())
# update the claims issue counter ::
# As this is preview for next claim, we need to add 1 to the returned
# claim counter
issue_data['claim_counter'] += 1
language = issue_data.get('vendor', {}).get('language')

response = {'recipient_suggestions': get_recipient_suggestions(record)}
Expand Down
3 changes: 1 addition & 2 deletions rero_ils/modules/items/views/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,4 @@ def issue_client_reference(issue_data):
holding_data.get('client_id'),
holding_data.get('order_reference')
]))
if parts:
return f'({"/".join(parts)})'
return f'({"/".join(parts)})' if parts else ''
30 changes: 23 additions & 7 deletions rero_ils/modules/notifications/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,34 @@ class Meta:

default_filter = None

def get_claims(self, item_pid):
"""Get the claims notifications about an issue item.
def _get_claims_query(self, item_pid):
"""Get the query to retrieve claim notifications about an issue.
:param item_pid: the item pid related to the claim notification.
:returns: a generator of claim notification hit from ES.
:rtype: generator<Hit>.
:returns: a ElasticSearch query object.
"""
query = self \
return self \
.filter('term', context__item__pid=item_pid) \
.filter('term', notification_type=NotificationType.CLAIM_ISSUE)
for hit in query.scan():
yield hit

def get_claims(self, item_pid):
"""Get the claims notifications about an issue item.
:param item_pid: the item pid related to the claim notification.
:returns: a generator of claim Notification object
:rtype: generator<Notification> | integer
"""
for hit in self._get_claims_query(item_pid).scan():
yield Notification.get_record(hit.meta.id)

def get_claims_count(self, item_pid):
"""Get the number of claims notifications about an issue item.
:param item_pid: the item pid related to the claim notification.
:returns: the number of claim notification
:rtype: int
"""
return self._get_claims_query(item_pid).count()


class Notification(IlsRecord, ABC):
Expand Down
18 changes: 14 additions & 4 deletions rero_ils/modules/notifications/dispatcher.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
#
# RERO ILS
# Copyright (C) 2019-2022 RERO
# Copyright (C) 2019-2023 RERO
# Copyright (C) 2019-2023 UCLouvain
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
Expand Down Expand Up @@ -143,12 +144,15 @@ def _process_notification(cls, notification, resend, aggregated):
aggregated[aggr_key].append(notification)

@staticmethod
def _create_email(recipients, reply_to, ctx_data, template):
def _create_email(recipients, reply_to, ctx_data, template,
cc=None, bcc=None):
"""Create email message from template.
:param recipients: List of emails to send the message too.
:param recipients: Main recipient emails list
:param reply_to: Reply to email address.
:param ctx_data: Dictionary with informations used in template.
:param cc: Email list where to send the message as "copy"
:param bcc: Email list where to send the message as "blind copy"
:param ctx_data: Dictionary with information used in template.
:param template: Template to use to create TemplatedMessage.
:returns: Message created.
"""
Expand All @@ -158,6 +162,8 @@ def _create_email(recipients, reply_to, ctx_data, template):
'noreply@rero.ch'),
reply_to=','.join(reply_to), # the client is unable to manage list
recipients=recipients,
cc=cc,
bcc=bcc,
ctx=ctx_data
)
# subject is the first line, body is the rest
Expand Down Expand Up @@ -264,6 +270,8 @@ def send_notification_by_email(notifications):
notification = notifications[0]
reply_to = notification.get_recipients(RecipientType.REPLY_TO)
recipients = notification.get_recipients(RecipientType.TO)
cc = notification.get_recipients(RecipientType.CC)
bcc = notification.get_recipients(RecipientType.BCC)

error_reasons = []
if not recipients:
Expand All @@ -282,6 +290,8 @@ def send_notification_by_email(notifications):

msg = Dispatcher._create_email(
recipients=recipients,
cc=cc,
bcc=bcc,
reply_to=reply_to,
ctx_data=context,
template=notification.get_template_path()
Expand Down
9 changes: 3 additions & 6 deletions rero_ils/modules/notifications/subclasses/circulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,14 @@ def get_communication_channel(self):

def get_language_to_use(self):
"""Get the language to use for dispatching the notification."""
# By default the language to use to build the notification is defined
# By default, the language to use to build the notification is defined
# in the patron setting. Override this method if the patron isn't the
# recipient of this notification.
return self.patron.get('patron', {}).get('communication_language')

def get_template_path(self):
"""Get the template to use to render the notification."""
# By default the template path to use reflects the notification type.
# By default, the template path to use reflects the notification type.
# Override this method if necessary
return f'email/{self.type}/{self.get_language_to_use()}.txt'

Expand All @@ -118,10 +118,7 @@ def get_recipients(self, address_type):
RecipientType.TO: self.get_recipients_to,
RecipientType.REPLY_TO: self.get_recipients_reply_to
}
try:
return mapping[address_type]()
except KeyError as e:
raise NotImplementedError() from e
return mapping[address_type]() if address_type in mapping else []

def get_recipients_reply_to(self):
"""Get the notification email address for 'REPLY_TO' recipient type."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"name",
"currency",
"vat_rate",
"organisation"
"organisation",
"communication_language"
],
"properties": {
"$schema": {
Expand Down
Loading

0 comments on commit 1f33e19

Please sign in to comment.