From 9f832011653bb88f8fa4a08c412469addbce2327 Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Mon, 7 Aug 2017 16:50:47 +0200 Subject: [PATCH] add sms delivery (#12665) --- corbo/models.py | 21 ++++- corbo/settings.py | 6 ++ corbo/utils.py | 31 +++++++ tests/test_api.py | 2 +- tests/{test_emailing.py => test_broadcasting.py} | 103 ++++++++++++++++++++++- 5 files changed, 157 insertions(+), 6 deletions(-) rename tests/{test_emailing.py => test_broadcasting.py} (54%) diff --git a/corbo/models.py b/corbo/models.py index 6ca195f..c519c38 100644 --- a/corbo/models.py +++ b/corbo/models.py @@ -18,6 +18,7 @@ from . import utils channel_choices = ( ('mailto', _('Email')), + ('sms', _('SMS')), ) @@ -150,16 +151,32 @@ class Broadcast(models.Model): def __unicode__(self): if self.deliver_time: - return u'announce {id} delivered via at {time}'.format( + return u'announce {id} delivered at {time}'.format( id=self.announce.id, time=self.deliver_time) return u'announce {id} to deliver'.format(id=self.announce.id) + def filter_destinations(self, destinations, prefix): + return [dest for dest in destinations if dest.startswith('%s:' % prefix)] + + def send_sms(self, title, content, destinations, category_id): + return utils.send_sms(content, destinations) + + def send_mailto(self, title, content, destinations, category_id): + return utils.send_email(title, content, destinations, category_id) + def send(self): + total_sent = 0 destinations = [s.identifier for s in self.announce.category.subscription_set.all() if s.identifier] - self.delivery_count = utils.send_email(self.announce.title, self.announce.text, destinations, category_id=self.announce.category.pk) + for channel_name, verbose_name in channel_choices: + action = getattr(self, 'send_' + channel_name) + filtered_destinations = self.filter_destinations(destinations, channel_name) + total_sent += action(self.announce.title, self.announce.text, + filtered_destinations, self.announce.category.id) + self.delivery_count = total_sent self.deliver_time = timezone.now() self.save() + class Meta: verbose_name = _('sent') ordering = ('-deliver_time',) diff --git a/corbo/settings.py b/corbo/settings.py index d4d6e76..62edfe8 100644 --- a/corbo/settings.py +++ b/corbo/settings.py @@ -162,6 +162,12 @@ REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] = ( # default site SITE_BASE_URL = 'http://localhost' +# default SMS Gateway +SMS_GATEWAY_URL = None + +# sms expeditor +SMS_EXPEDITOR = 'Corbo' + local_settings_file = os.environ.get('CORBO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')) if os.path.exists(local_settings_file): diff --git a/corbo/utils.py b/corbo/utils.py index 2bb97a0..3c15f04 100644 --- a/corbo/utils.py +++ b/corbo/utils.py @@ -16,6 +16,7 @@ import os import logging +import requests import urlparse import hashlib from html2text import HTML2Text @@ -72,3 +73,33 @@ def send_email(title, content, destinations, category_id): logger.warning('Error occured while sending announce "%s" to %s.', title, dest) return total_sent + +def send_sms(content, destinations): + from django.conf import settings + logger = logging.getLogger(__name__) + sent = 0 + if not destinations: + return sent + if settings.SMS_GATEWAY_URL: + # remove all HTML formatting from content + html_content = etree.HTML(content) + # remove identifier prefix + destinations = [d.replace('sms:', '') for d in destinations] + data = {'to': destinations, + 'message': etree.tostring(html_content, method='text'), + 'from': settings.SMS_EXPEDITOR} + try: + response = requests.post(settings.SMS_GATEWAY_URL, json=data) + response.raise_for_status() + if not response.json()['err']: + # if no error returned by SMS gateway presume the that content + # was delivered to all destinations + sent = len(destinations) + else: + logger.warning('Error occured while sending sms: %s', response.json()['err_desc']) + except requests.RequestException as e: + logger.warning('Failed to reach SMS gateway: %s', e) + return sent + else: + logger.error('SMS send requested but no SMS gateway defined.') + return sent diff --git a/tests/test_api.py b/tests/test_api.py index b557022..2caed9b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -59,7 +59,7 @@ def test_get_newsletters(app, categories, announces, user): assert category['id'] in [slugify(c) for c in CATEGORIES] assert category['text'] in CATEGORIES assert 'transports' in category - assert category['transports'] == [{'id': 'mailto', 'text': 'Email'}] + assert category['transports'] == [{'id': 'mailto', 'text': 'Email'}, {'id': 'sms', 'text': 'SMS'}] def test_get_subscriptions_by_email(app, categories, announces, user): diff --git a/tests/test_emailing.py b/tests/test_broadcasting.py similarity index 54% rename from tests/test_emailing.py rename to tests/test_broadcasting.py index 255a629..58bb18f 100644 --- a/tests/test_emailing.py +++ b/tests/test_broadcasting.py @@ -3,13 +3,17 @@ from uuid import uuid4 import os import re import urllib +import logging import mock +import random +import requests from django.core.urlresolvers import reverse from django.core import mail, signing from django.utils import timezone from django.core.files.storage import DefaultStorage from django.utils.text import slugify +from django.test import override_settings from corbo.models import Category, Announce, Subscription, Broadcast from corbo.models import channel_choices @@ -18,6 +22,12 @@ pytestmark = pytest.mark.django_db CATEGORIES = (u'Alerts', u'News') +def get_random_number(): + number_generator = range(10) + random.shuffle(number_generator) + number = ''.join(map(str, number_generator)) + return number + @pytest.fixture def categories(): @@ -57,7 +67,7 @@ def test_send_email(app, categories, announces, mailoutbox): for category in categories: uuid = uuid4() Subscription.objects.create(category=category, - identifier='%s@example.net' % uuid, uuid=uuid) + identifier='mailto:%s@example.net' % uuid, uuid=uuid) for i, announce in enumerate(announces): broadcast = Broadcast.objects.get(announce=announce) broadcast.send() @@ -72,7 +82,7 @@ def test_check_inline_css(app, categories, announces, mailoutbox): announce.save() uuid = uuid4() Subscription.objects.create(category=announce.category, - identifier='%s@example.net' % uuid, uuid=uuid) + identifier='mailto:%s@example.net' % uuid, uuid=uuid) broadcast = Broadcast.objects.get(announce=announce) broadcast.send() assert broadcast.delivery_count @@ -94,7 +104,7 @@ def test_check_inline_images(mocked_get, app, categories, announces, mailoutbox) announce.save() uuid = uuid4() Subscription.objects.create(category=announce.category, - identifier='%s@example.net' % uuid, uuid=uuid) + identifier='mailto:%s@example.net' % uuid, uuid=uuid) broadcast = Broadcast.objects.get(announce=announce) mocked_get.return_value = mock.Mock(status_code=200, headers={'content-type': 'image/png'}, @@ -145,3 +155,90 @@ def test_unsubscription_link(app, categories, announces, custom_mailoutbox): # make sure the uri schema is not in the page resp = app.get(unsubscription_link) assert scheme not in resp.content + +def test_send_sms_with_no_gateway_defined(app, categories, announces, caplog): + for category in categories: + uuid = uuid4() + Subscription.objects.create(category=category, + identifier='sms:%s' % get_random_number(), uuid=uuid) + for i, announce in enumerate(announces): + broadcast = Broadcast.objects.get(announce=announce) + broadcast.send() + assert broadcast.delivery_count == 0 + records = caplog.records() + for record in records: + assert record.name == 'corbo.utils' + assert record.levelno == logging.ERROR + assert record.getMessage() == 'SMS send requested but no SMS gateway defined.' + +@mock.patch('corbo.utils.requests.post') +def test_send_sms_with_gateway_api_error(mocked_post, app, categories, announces, caplog): + for category in categories: + for i in range(3): + uuid = uuid4() + Subscription.objects.create(category=category, + identifier='sms:%s' % get_random_number(), uuid=uuid) + for i, announce in enumerate(announces): + broadcast = Broadcast.objects.get(announce=announce) + with override_settings(SMS_GATEWAY_URL='http://sms.gateway'): + mocked_response = mock.Mock() + mocked_response.json.return_value = {'err': 1, 'data': None, + 'err_desc': 'Payload error: missing "message" in JSON payload'} + mocked_post.return_value = mocked_response + broadcast.send() + assert broadcast.delivery_count == 0 + records = caplog.records() + assert len(records) == 1 + i + for record in records: + assert record.name == 'corbo.utils' + assert record.levelno == logging.WARNING + assert record.getMessage() == 'Error occured while sending sms: Payload error: missing "message" in JSON payload' + +@mock.patch('corbo.utils.requests.post') +def test_send_sms_with_gateway_connection_error(mocked_post, app, categories, announces, caplog): + for category in categories: + for i in range(3): + uuid = uuid4() + Subscription.objects.create(category=category, + identifier='sms:%s' % get_random_number(), uuid=uuid) + for i, announce in enumerate(announces): + broadcast = Broadcast.objects.get(announce=announce) + with override_settings(SMS_GATEWAY_URL='http://sms.gateway'): + mocked_response = mock.Mock() + def mocked_requests_connection_error(*args, **kwargs): + raise requests.ConnectionError('unreachable') + mocked_post.side_effect = mocked_requests_connection_error + mocked_post.return_value = mocked_response + broadcast.send() + assert broadcast.delivery_count == 0 + records = caplog.records() + assert len(records) == 1 + i + for record in records: + assert record.name == 'corbo.utils' + assert record.levelno == logging.WARNING + assert record.getMessage() == 'Failed to reach SMS gateway: unreachable' + +@mock.patch('corbo.utils.requests.post') +def test_send_sms(mocked_post, app, categories, announces): + for category in categories: + for i in range(3): + uuid = uuid4() + Subscription.objects.create(category=category, + identifier='sms:%s' % get_random_number(), uuid=uuid) + for announce in announces: + broadcast = Broadcast.objects.get(announce=announce) + with override_settings(SMS_GATEWAY_URL='http://sms.gateway'): + mocked_response = mock.Mock() + mocked_response.json.return_value = {'err': 0, 'err_desc': None, 'data': 'gateway response'} + + for announce in announces: + broadcast = Broadcast.objects.get(announce=announce) + with override_settings(SMS_GATEWAY_URL='http://sms.gateway'): + mocked_response = mock.Mock() + mocked_response.json.return_value = {'err': 0, 'err_desc': None, 'data': 'gateway response'} + mocked_post.return_value = mocked_response + broadcast.send() + assert mocked_post.call_args[0][0] == 'http://sms.gateway' + assert mocked_post.call_args[1]['json']['from'] == 'Corbo' + assert isinstance(mocked_post.call_args[1]['json']['to'], list) + assert broadcast.delivery_count == 3 -- 2.15.0