From db114e40a210e8ddd3f551e136d724361868bc25 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 | 13 ++- corbo/settings.py | 6 ++ corbo/utils.py | 27 ++++++ corbo/views.py | 2 +- requirements.txt | 1 + tests/test_api.py | 2 +- tests/{test_emailing.py => test_broadcasting.py} | 100 ++++++++++++++++++++++- 7 files changed, 144 insertions(+), 7 deletions(-) rename tests/{test_emailing.py => test_broadcasting.py} (55%) diff --git a/corbo/models.py b/corbo/models.py index 6ca195f..f826d86 100644 --- a/corbo/models.py +++ b/corbo/models.py @@ -18,6 +18,7 @@ from . import utils channel_choices = ( ('mailto', _('Email')), + ('sms', _('SMS')), ) @@ -156,7 +157,17 @@ class Broadcast(models.Model): def send(self): 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) + emails = [] + mobile_numbers = [] + for destination in destinations: + if destination.startswith('mailto:'): + emails.append(destination.replace('mailto:', '')) + elif destination.startswith('sms:'): + mobile_numbers.append(destination.replace('sms:', '')) + self.delivery_count += utils.send_email(self.announce.title, self.announce.text, + emails, category_id=self.announce.category.pk) + + self.delivery_count += utils.send_sms(self.announce.text, mobile_numbers) self.deliver_time = timezone.now() self.save() 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..4b08273 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,29 @@ 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) + 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 + return sent diff --git a/corbo/views.py b/corbo/views.py index c61e58b..390ec2f 100644 --- a/corbo/views.py +++ b/corbo/views.py @@ -165,7 +165,7 @@ class UnsubscribeView(DeleteView): data = signing.loads(self.kwargs['unsubscription_token']) try: return models.Subscription.objects.get(category__pk=data['category'], - identifier=data['identifier']) + identifier='mailto:%(identifier)s' % data) except models.Subscription.DoesNotExist: raise Http404 diff --git a/requirements.txt b/requirements.txt index f134314..7a3bd12 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ emails feedparser requests lxml +nltk -e git+http://repos.entrouvert.org/gadjo.git/#egg=gadjo 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 55% rename from tests/test_emailing.py rename to tests/test_broadcasting.py index 255a629..1432070 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'}, @@ -139,9 +149,91 @@ def test_unsubscription_link(app, categories, announces, custom_mailoutbox): assert unsubscription_link in mail.outbox[index].text assert unsubscription_link_sentinel != unsubscription_link assert signing.loads(signature) == { - 'category': announce.category.pk, 'identifier': destination.identifier} + 'category': announce.category.pk, 'identifier': destination.identifier.replace(scheme, '')} unsubscription_link_sentinel = unsubscription_link # 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): + 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 + +@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