-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* First draft of whatsapp notifications refactor * User form template update * CKAN core requires timezone naive datetimes * Test whatsapp notifications generated * Send whatsapp messages * Validate user plugin extras * Validate phone number on keyup * Test user extras validation * Allow user to disable sms or whatsapp notifications when calling the action * Rename action to send_phone_notifications * Test messages comply with whatsapp templates
- Loading branch information
1 parent
bb29619
commit 127dce4
Showing
15 changed files
with
569 additions
and
400 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,64 +0,0 @@ | ||
import ckan.plugins.toolkit as toolkit | ||
import ckan.logic as logic | ||
from ckanext.activity.email_notifications import send_notification | ||
import ckanext.dataset_subscriptions.helpers as helpers | ||
import ckanext.activity.email_notifications as email_notifications | ||
import ckan.model as model | ||
import ckan.lib.base as base | ||
from ckan.common import config | ||
import unihandecode | ||
|
||
|
||
@toolkit.chained_action | ||
@toolkit.side_effect_free | ||
def send_email_notifications(original_action, context, data_dict): | ||
email_notifications._notifications_functions = [dms_notification_provider] | ||
email_notifications.send_notification = latin_username_send_notification | ||
return original_action(context, data_dict) | ||
|
||
|
||
def latin_username_send_notification(user, email_dict): | ||
# fix for AWS SES not supporting UTF8 encoding of recepient field | ||
# https://docs.aws.amazon.com/cli/latest/reference/ses/send-email.html | ||
user['display_name'] = unihandecode.unidecode(user['display_name']) | ||
return send_notification(user, email_dict) | ||
|
||
|
||
def dms_notification_provider(user_dict, since): | ||
if not user_dict.get('activity_streams_email_notifications'): | ||
return [] | ||
context = {'model': model, 'session': model.Session, | ||
'user': user_dict['id']} | ||
activity_list = logic.get_action('dashboard_activity_list')(context, {}) | ||
dataset_activity_list = [activity for activity in activity_list | ||
if activity['user_id'] != user_dict['id'] | ||
and 'package' in activity['activity_type']] | ||
# We want a notification per changed dataset, not a list of all changes | ||
timestamp_sorted_activity_list = sorted(dataset_activity_list, | ||
key=lambda item: item['timestamp']) | ||
deduplicated_activity_list = list({item["object_id"]: | ||
item for item in timestamp_sorted_activity_list}.values()) | ||
activity_list_with_dataset_name = helpers.add_dataset_details_to_activity_list(deduplicated_activity_list, context) | ||
recent_activity_list = helpers.filter_out_old_activites(activity_list_with_dataset_name, since) | ||
return dms_notifications_for_activities(recent_activity_list, user_dict) | ||
|
||
|
||
def dms_notifications_for_activities(activities, user_dict): | ||
if not activities: | ||
return [] | ||
if not user_dict.get('activity_streams_email_notifications'): | ||
return [] | ||
subject = toolkit.ungettext( | ||
"{n} new notification from Department of HIV and AIDS Document Management System", | ||
"{n} new notifications from Department of HIV and AIDS Document Management System", | ||
len(activities)).format( | ||
site_title=config.get('ckan.site_title'), | ||
n=len(activities)) | ||
body = base.render( | ||
'dataset-subscriptions_email_body.j2', | ||
extra_vars={'activities': activities}) | ||
notifications = [{ | ||
'subject': subject, | ||
'body': body | ||
}] | ||
return notifications | ||
58 changes: 58 additions & 0 deletions
58
ckanext/dataset_subscriptions/actions/email_notifications.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import ckan.plugins.toolkit as toolkit | ||
import ckanext.dataset_subscriptions.helpers as helpers | ||
from ckanext.activity.email_notifications import send_notification | ||
import ckanext.activity.email_notifications as email_notifications | ||
import unihandecode | ||
|
||
|
||
@toolkit.chained_action | ||
@toolkit.side_effect_free | ||
def send_email_notifications(original_action, context, data_dict): | ||
email_notifications._notifications_functions = [dms_notification_provider] | ||
email_notifications.send_notification = latin_username_send_notification | ||
return original_action(context, data_dict) | ||
|
||
|
||
def latin_username_send_notification(user, email_dict): | ||
# fix for AWS SES not supporting UTF8 encoding of recepient field | ||
# https://docs.aws.amazon.com/cli/latest/reference/ses/send-email.html | ||
user['display_name'] = unihandecode.unidecode(user['display_name']) | ||
return send_notification(user, email_dict) | ||
|
||
|
||
def dms_notification_provider(user_dict, since): | ||
if not user_dict.get('activity_streams_email_notifications'): | ||
return [] | ||
activity_list = toolkit.get_action('dashboard_activity_list')({'user': user_dict['id']}, {}) | ||
dataset_activity_list = [activity for activity in activity_list | ||
if activity['user_id'] != user_dict['id'] | ||
and 'package' in activity['activity_type']] | ||
# We want a notification per changed dataset, not a list of all changes | ||
timestamp_sorted_activity_list = sorted(dataset_activity_list, | ||
key=lambda item: item['timestamp']) | ||
deduplicated_activity_list = list({item["object_id"]: | ||
item for item in timestamp_sorted_activity_list}.values()) | ||
activity_list_with_dataset_name = helpers.add_dataset_details_to_activity_list(deduplicated_activity_list) | ||
recent_activity_list = helpers.filter_out_old_activites(activity_list_with_dataset_name, since) | ||
return dms_notifications_for_activities(recent_activity_list, user_dict) | ||
|
||
|
||
def dms_notifications_for_activities(activities, user_dict): | ||
if not activities: | ||
return [] | ||
if not user_dict.get('activity_streams_email_notifications'): | ||
return [] | ||
subject = toolkit.ungettext( | ||
"{n} new notification from {site_title}", | ||
"{n} new notifications from {site_title}", | ||
len(activities)).format( | ||
site_title=toolkit.config.get('ckan.site_title'), | ||
n=len(activities)) | ||
body = toolkit.render( | ||
'dataset-subscriptions_email_body.j2', | ||
extra_vars={'activities': activities}) | ||
notifications = [{ | ||
'subject': subject, | ||
'body': body | ||
}] | ||
return notifications |
139 changes: 139 additions & 0 deletions
139
ckanext/dataset_subscriptions/actions/phone_notifications.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import logging | ||
from ckan.plugins import toolkit | ||
from twilio.rest import Client | ||
from twilio.base.exceptions import TwilioRestException | ||
from datetime import timedelta, datetime | ||
from ckanext.dataset_subscriptions import helpers | ||
|
||
|
||
ACCOUNT_SID = toolkit.config.get('ckanext.dataset_subscriptions.twilio_account_sid') | ||
AUTH_TOKEN = toolkit.config.get('ckanext.dataset_subscriptions.twilio_auth_token') | ||
SMS_SENDER_NR = toolkit.config.get('ckanext.dataset_subscriptions.sms_sender_nr') | ||
WHATSAPP_SENDER_NR = toolkit.config.get('ckanext.dataset_subscriptions.whatsapp_sender_nr') | ||
|
||
|
||
client = Client(ACCOUNT_SID, AUTH_TOKEN) | ||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def send_phone_notifications(context, data_dict): | ||
""" | ||
Sends SMS and Whatsapp notifications via the Twilio API. Both notification types | ||
have been combined into a single action since they share a great deal of logic | ||
which requires a loop through all site users. | ||
:param send_sms: Send SMS messages (optional, default: ``True``) | ||
:type send_sms: bool | ||
:param send_whatsapp: Send Whatsapp messages (optional, default: ``True``) | ||
:type send_whatsapp: bool | ||
""" | ||
message_sids = [] | ||
toolkit.check_access('send_email_notifications', context, data_dict) | ||
users = toolkit.get_action('user_list')( | ||
{'ignore_auth': True}, | ||
{'all_fields': True, 'include_plugin_extras': True} | ||
) | ||
for user in users: | ||
if _twilio_notifications_enabled(user, data_dict): | ||
recent_activities = _get_recent_activity_list(user, context) | ||
if recent_activities: | ||
if _sms_notifications_enabled(user, data_dict): | ||
message_sids.append(_send_message( | ||
_create_sms_message(recent_activities), | ||
SMS_SENDER_NR, | ||
user['phonenumber'] | ||
)) | ||
if _whatsapp_notifications_enabled(user, data_dict): | ||
message_sids.append(_send_message( | ||
_create_message_header(recent_activities), | ||
f"whatsapp:{WHATSAPP_SENDER_NR}", | ||
f"whatsapp:{user['phonenumber']}" | ||
)) | ||
return message_sids | ||
|
||
|
||
def _sms_notifications_enabled(user_dict, data_dict): | ||
action_send_sms = toolkit.asbool(data_dict.get('send_sms', True)) | ||
user_enabled_sms = toolkit.asbool(user_dict.get("activity_streams_sms_notifications")) | ||
if action_send_sms and user_enabled_sms and user_dict.get("phonenumber"): | ||
return True | ||
return False | ||
|
||
|
||
def _whatsapp_notifications_enabled(user_dict, data_dict): | ||
action_send_whatsapp = toolkit.asbool(data_dict.get('send_whatsapp', True)) | ||
user_enabled_whatsapp = toolkit.asbool(user_dict.get("activity_streams_whatsapp_notifications")) | ||
if action_send_whatsapp and user_enabled_whatsapp and user_dict.get("phonenumber"): | ||
return True | ||
return False | ||
|
||
|
||
def _twilio_notifications_enabled(user_dict, data_dict): | ||
if _sms_notifications_enabled(user_dict, data_dict) or \ | ||
_whatsapp_notifications_enabled(user_dict, data_dict): | ||
return True | ||
return False | ||
|
||
|
||
def _twilio_notification_time_delta_utc(): | ||
since_hours = toolkit.config.get('ckanext.dataset_subscriptions.sms_notifications_hours_since', 1) | ||
since_delta = timedelta(hours=int(since_hours)) | ||
since_datetime = (datetime.utcnow() - since_delta) | ||
return since_datetime | ||
|
||
|
||
def _get_recent_activity_list(user_dict, context): | ||
# Only raise notifications for activities since last message, or last view of the dashboard | ||
dashboard_last_viewed = (context['model'].Dashboard.get(user_dict['id']).activity_stream_last_viewed) | ||
since = max(_twilio_notification_time_delta_utc(), dashboard_last_viewed) | ||
# Get activities for only changes to datasets, within the desired period, not made by notifiee | ||
activity_list = toolkit.get_action('dashboard_activity_list')({'user': user_dict['id']}, {}) | ||
dataset_activity_list = [activity for activity in activity_list | ||
if activity['user_id'] != user_dict['id'] | ||
and 'package' in activity['activity_type']] | ||
# We want the latest notification per changed dataset, not all changes | ||
timestamp_sorted_activity_list = sorted(dataset_activity_list, key=lambda item: item['timestamp']) | ||
deduplicated_activity_list = list({ | ||
item["object_id"]: item for item in timestamp_sorted_activity_list | ||
}.values()) | ||
activity_list_with_dataset_name = helpers.add_dataset_details_to_activity_list(deduplicated_activity_list) | ||
recent_activity_list = helpers.filter_out_old_activites(activity_list_with_dataset_name, since) | ||
return recent_activity_list | ||
|
||
|
||
def _create_sms_message(activities): | ||
nr_of_datasets_to_display = toolkit.config.get( | ||
'ckanext.dataset_subscriptions.sms_nr_of_datasets_to_display', 2 | ||
) | ||
header = _create_message_header(activities) | ||
return toolkit.render( | ||
'dataset-subscriptions_sms_body.j2', | ||
extra_vars={ | ||
'activities': activities, | ||
'header': header, | ||
'nr_of_datasets_to_display': nr_of_datasets_to_display | ||
} | ||
) | ||
|
||
|
||
def _create_message_header(activities): | ||
return toolkit.ungettext( | ||
"{n} dataset that you are following has recently been updated in {site_title}", | ||
"{n} datasets that you are following have recently been updated in {site_title}", | ||
len(activities)).format( | ||
site_title=toolkit.config.get('ckan.site_title'), | ||
n=len(activities) | ||
) | ||
|
||
|
||
def _send_message(message_body, sender, phonenumber): | ||
try: | ||
message = client.messages.create( | ||
from_=sender, | ||
body=message_body, | ||
to=phonenumber | ||
) | ||
except TwilioRestException: | ||
logger.exception(f"Failed to send sms message to {phonenumber}", exc_info=True) | ||
return | ||
return message.sid |
Oops, something went wrong.