Projet

Général

Profil

« Précédent | Suivant » 

Révision 9f832011

Ajouté par Serghei Mihai il y a plus de 6 ans

add sms delivery (#12665)

Voir les différences:

corbo/models.py
18 18

  
19 19
channel_choices = (
20 20
    ('mailto', _('Email')),
21
    ('sms', _('SMS')),
21 22
)
22 23

  
23 24

  
......
150 151

  
151 152
    def __unicode__(self):
152 153
        if self.deliver_time:
153
            return u'announce {id} delivered via at {time}'.format(
154
            return u'announce {id} delivered at {time}'.format(
154 155
                id=self.announce.id, time=self.deliver_time)
155 156
        return u'announce {id} to deliver'.format(id=self.announce.id)
156 157

  
158
    def filter_destinations(self, destinations, prefix):
159
        return [dest for dest in destinations if dest.startswith('%s:' % prefix)]
160

  
161
    def send_sms(self, title, content, destinations, category_id):
162
        return utils.send_sms(content, destinations)
163

  
164
    def send_mailto(self, title, content, destinations, category_id):
165
        return utils.send_email(title, content, destinations, category_id)
166

  
157 167
    def send(self):
168
        total_sent = 0
158 169
        destinations = [s.identifier for s in self.announce.category.subscription_set.all() if s.identifier]
159
        self.delivery_count = utils.send_email(self.announce.title, self.announce.text, destinations, category_id=self.announce.category.pk)
170
        for channel_name, verbose_name in channel_choices:
171
            action = getattr(self, 'send_' + channel_name)
172
            filtered_destinations = self.filter_destinations(destinations, channel_name)
173
            total_sent += action(self.announce.title, self.announce.text,
174
                                 filtered_destinations, self.announce.category.id)
175
        self.delivery_count = total_sent
160 176
        self.deliver_time = timezone.now()
161 177
        self.save()
162 178

  
179

  
163 180
    class Meta:
164 181
        verbose_name = _('sent')
165 182
        ordering = ('-deliver_time',)
corbo/settings.py
162 162
# default site
163 163
SITE_BASE_URL = 'http://localhost'
164 164

  
165
# default SMS Gateway
166
SMS_GATEWAY_URL = None
167

  
168
# sms expeditor
169
SMS_EXPEDITOR = 'Corbo'
170

  
165 171
local_settings_file = os.environ.get('CORBO_SETTINGS_FILE',
166 172
        os.path.join(os.path.dirname(__file__), 'local_settings.py'))
167 173
if os.path.exists(local_settings_file):
corbo/utils.py
16 16

  
17 17
import os
18 18
import logging
19
import requests
19 20
import urlparse
20 21
import hashlib
21 22
from html2text import HTML2Text
......
72 73
            logger.warning('Error occured while sending announce "%s" to %s.',
73 74
                           title, dest)
74 75
    return total_sent
76

  
77
def send_sms(content, destinations):
78
    from django.conf import settings
79
    logger = logging.getLogger(__name__)
80
    sent = 0
81
    if not destinations:
82
        return sent
83
    if settings.SMS_GATEWAY_URL:
84
        # remove all HTML formatting from content
85
        html_content = etree.HTML(content)
86
        # remove identifier prefix
87
        destinations = [d.replace('sms:', '') for d in destinations]
88
        data = {'to': destinations,
89
                'message': etree.tostring(html_content, method='text'),
90
                'from': settings.SMS_EXPEDITOR}
91
        try:
92
            response = requests.post(settings.SMS_GATEWAY_URL, json=data)
93
            response.raise_for_status()
94
            if not response.json()['err']:
95
                # if no error returned by SMS gateway presume the that content
96
                # was delivered to all destinations
97
                sent = len(destinations)
98
            else:
99
                logger.warning('Error occured while sending sms: %s', response.json()['err_desc'])
100
        except requests.RequestException as e:
101
            logger.warning('Failed to reach SMS gateway: %s', e)
102
            return sent
103
    else:
104
        logger.error('SMS send requested but no SMS gateway defined.')
105
    return sent
tests/test_api.py
59 59
        assert category['id'] in [slugify(c) for c in CATEGORIES]
60 60
        assert category['text'] in CATEGORIES
61 61
        assert 'transports' in category
62
        assert category['transports'] == [{'id': 'mailto', 'text': 'Email'}]
62
        assert category['transports'] == [{'id': 'mailto', 'text': 'Email'}, {'id': 'sms', 'text': 'SMS'}]
63 63

  
64 64

  
65 65
def test_get_subscriptions_by_email(app, categories, announces, user):
tests/test_broadcasting.py
1
import pytest
2
from uuid import uuid4
3
import os
4
import re
5
import urllib
6
import logging
7
import mock
8
import random
9
import requests
10

  
11
from django.core.urlresolvers import reverse
12
from django.core import mail, signing
13
from django.utils import timezone
14
from django.core.files.storage import DefaultStorage
15
from django.utils.text import slugify
16
from django.test import override_settings
17

  
18
from corbo.models import Category, Announce, Subscription, Broadcast
19
from corbo.models import channel_choices
20

  
21
pytestmark = pytest.mark.django_db
22

  
23
CATEGORIES = (u'Alerts', u'News')
24

  
25
def get_random_number():
26
    number_generator = range(10)
27
    random.shuffle(number_generator)
28
    number = ''.join(map(str, number_generator))
29
    return number
30

  
31

  
32
@pytest.fixture
33
def categories():
34
    categories = []
35
    for category in CATEGORIES:
36
        c, created = Category.objects.get_or_create(name=category, slug=slugify(category))
37
        categories.append(c)
38
    return categories
39

  
40

  
41
@pytest.fixture
42
def announces():
43
    announces = []
44
    for category in Category.objects.all():
45
        a = Announce.objects.create(category=category, title='Announce 1',
46
                                    publication_time=timezone.now(),
47
                                    text='<h2>Announce 1</h2>')
48
        Broadcast.objects.create(announce=a)
49
        announces.append(a)
50
        a = Announce.objects.create(category=category, title='Announce 2',
51
                                    publication_time=timezone.now(),
52
                                    text='<h2>Announce 2</h2>')
53
        Broadcast.objects.create(announce=a)
54
        announces.append(a)
55
    return announces
56

  
57

  
58
def test_emailing_with_no_subscriptions(app, categories, announces, mailoutbox):
59
    for announce in announces:
60
        broadcast = Broadcast.objects.get(announce=announce)
61
        broadcast.send()
62
        assert not broadcast.delivery_count
63
    assert not len(mailoutbox)
64

  
65

  
66
def test_send_email(app, categories, announces, mailoutbox):
67
    for category in categories:
68
        uuid = uuid4()
69
        Subscription.objects.create(category=category,
70
                                    identifier='mailto:%s@example.net' % uuid, uuid=uuid)
71
    for i, announce in enumerate(announces):
72
        broadcast = Broadcast.objects.get(announce=announce)
73
        broadcast.send()
74
        assert broadcast.delivery_count
75
        assert len(mailoutbox) == i+1
76

  
77

  
78
def test_check_inline_css(app, categories, announces, mailoutbox):
79
    total_sent = 0
80
    for i, announce in enumerate(announces):
81
        announce.text = '<style type="text/css">h2 {color: #F00}</style>' + announce.text
82
        announce.save()
83
        uuid = uuid4()
84
        Subscription.objects.create(category=announce.category,
85
                                    identifier='mailto:%s@example.net' % uuid, uuid=uuid)
86
        broadcast = Broadcast.objects.get(announce=announce)
87
        broadcast.send()
88
        assert broadcast.delivery_count
89
        assert len(mailoutbox) == total_sent + broadcast.delivery_count
90
        total_sent += broadcast.delivery_count
91
        assert 'h2 style="color:#F00"' in mailoutbox[i].html
92

  
93

  
94
@mock.patch('emails.utils.requests.get')
95
def test_check_inline_images(mocked_get, app, categories, announces, mailoutbox):
96
    storage = DefaultStorage()
97
    media_path = os.path.join(os.path.dirname(__file__), 'media')
98
    image_name = 'logo.png'
99
    image_name = storage.save(image_name, file(os.path.join(media_path, image_name)))
100
    total_sent = 0
101
    for i, announce in enumerate(announces):
102
        img_src = "/media/%s" % image_name
103
        announce.text = announce.text + '<img src="%s" />' % img_src
104
        announce.save()
105
        uuid = uuid4()
106
        Subscription.objects.create(category=announce.category,
107
                                    identifier='mailto:%s@example.net' % uuid, uuid=uuid)
108
        broadcast = Broadcast.objects.get(announce=announce)
109
        mocked_get.return_value = mock.Mock(status_code=200,
110
                                            headers={'content-type': 'image/png'},
111
                                            content=storage.open(image_name).read())
112
        broadcast.send()
113
        assert broadcast.delivery_count
114

  
115
        assert len(mailoutbox) == total_sent + broadcast.delivery_count
116
        attachments = [a['filename'] for a in mailoutbox[0].attachments.as_dict()]
117
        assert image_name in attachments
118
        assert 'cid:%s' % image_name in mail.outbox[0].html_body
119
        assert 'cid:%s' % image_name in mail.outbox[0].text_body
120
        total_sent += broadcast.delivery_count
121
    storage.delete(image_name)
122

  
123

  
124
def test_unsubscription_link(app, categories, announces, custom_mailoutbox):
125
    unsubscription_link_sentinel = ''
126
    subscriptions_number = 3
127
    scheme = 'mailto:'
128
    for category in categories:
129
        for i in xrange(subscriptions_number):
130
            uuid = uuid4()
131
            uri = scheme + '%s@example.com' % uuid
132
            Subscription.objects.create(category=category, identifier=uri, uuid=str(uuid))
133

  
134
        for i, announce in enumerate(announces):
135
            if announce.category != category:
136
                continue
137
            broadcast = Broadcast.objects.get(announce=announce)
138
            broadcast.send()
139
            assert broadcast.delivery_count
140
            assert len(mail.outbox) == (i+1)*subscriptions_number
141
            assert mail.outbox[i*subscriptions_number].subject == announce.title
142

  
143
            for counter, destination in enumerate(category.subscription_set.all()):
144
                index = i*subscriptions_number+counter
145
                signature = urllib.unquote(re.findall('/unsubscribe/(.*)"', mail.outbox[index].html)[0])
146
                unsubscription_link = reverse('unsubscribe', kwargs={'unsubscription_token': signature})
147
                assert mail.outbox[index]._headers['List-Unsubscribe'] == '<http://localhost%s>' % unsubscription_link
148
                assert unsubscription_link in mail.outbox[index].html
149
                assert unsubscription_link in mail.outbox[index].text
150
                assert unsubscription_link_sentinel != unsubscription_link
151
                assert signing.loads(signature) == {
152
                    'category': announce.category.pk, 'identifier': destination.identifier}
153
                unsubscription_link_sentinel = unsubscription_link
154

  
155
                # make sure the uri schema is not in the page
156
                resp = app.get(unsubscription_link)
157
                assert scheme not in resp.content
158

  
159
def test_send_sms_with_no_gateway_defined(app, categories, announces, caplog):
160
    for category in categories:
161
        uuid = uuid4()
162
        Subscription.objects.create(category=category,
163
                                    identifier='sms:%s' % get_random_number(), uuid=uuid)
164
    for i, announce in enumerate(announces):
165
        broadcast = Broadcast.objects.get(announce=announce)
166
        broadcast.send()
167
        assert broadcast.delivery_count == 0
168
        records = caplog.records()
169
        for record in records:
170
            assert record.name == 'corbo.utils'
171
            assert record.levelno == logging.ERROR
172
            assert record.getMessage() == 'SMS send requested but no SMS gateway defined.'
173

  
174
@mock.patch('corbo.utils.requests.post')
175
def test_send_sms_with_gateway_api_error(mocked_post, app, categories, announces, caplog):
176
    for category in categories:
177
        for i in range(3):
178
            uuid = uuid4()
179
            Subscription.objects.create(category=category,
180
                            identifier='sms:%s' % get_random_number(), uuid=uuid)
181
    for i, announce in enumerate(announces):
182
        broadcast = Broadcast.objects.get(announce=announce)
183
        with override_settings(SMS_GATEWAY_URL='http://sms.gateway'):
184
            mocked_response = mock.Mock()
185
            mocked_response.json.return_value = {'err': 1, 'data': None,
186
                            'err_desc': 'Payload error: missing "message" in JSON payload'}
187
            mocked_post.return_value = mocked_response
188
            broadcast.send()
189
            assert broadcast.delivery_count == 0
190
            records = caplog.records()
191
            assert len(records) == 1 + i
192
            for record in records:
193
                assert record.name == 'corbo.utils'
194
                assert record.levelno == logging.WARNING
195
                assert record.getMessage() == 'Error occured while sending sms: Payload error: missing "message" in JSON payload'
196

  
197
@mock.patch('corbo.utils.requests.post')
198
def test_send_sms_with_gateway_connection_error(mocked_post, app, categories, announces, caplog):
199
    for category in categories:
200
        for i in range(3):
201
            uuid = uuid4()
202
            Subscription.objects.create(category=category,
203
                            identifier='sms:%s' % get_random_number(), uuid=uuid)
204
    for i, announce in enumerate(announces):
205
        broadcast = Broadcast.objects.get(announce=announce)
206
        with override_settings(SMS_GATEWAY_URL='http://sms.gateway'):
207
            mocked_response = mock.Mock()
208
            def mocked_requests_connection_error(*args, **kwargs):
209
                raise requests.ConnectionError('unreachable')
210
            mocked_post.side_effect = mocked_requests_connection_error
211
            mocked_post.return_value = mocked_response
212
            broadcast.send()
213
            assert broadcast.delivery_count == 0
214
            records = caplog.records()
215
            assert len(records) == 1 + i
216
            for record in records:
217
                assert record.name == 'corbo.utils'
218
                assert record.levelno == logging.WARNING
219
                assert record.getMessage() == 'Failed to reach SMS gateway: unreachable'
220

  
221
@mock.patch('corbo.utils.requests.post')
222
def test_send_sms(mocked_post, app, categories, announces):
223
    for category in categories:
224
        for i in range(3):
225
            uuid = uuid4()
226
            Subscription.objects.create(category=category,
227
                            identifier='sms:%s' % get_random_number(), uuid=uuid)
228
    for announce in announces:
229
        broadcast = Broadcast.objects.get(announce=announce)
230
        with override_settings(SMS_GATEWAY_URL='http://sms.gateway'):
231
            mocked_response = mock.Mock()
232
            mocked_response.json.return_value = {'err': 0, 'err_desc': None, 'data': 'gateway response'}
233

  
234
    for announce in announces:
235
        broadcast = Broadcast.objects.get(announce=announce)
236
        with override_settings(SMS_GATEWAY_URL='http://sms.gateway'):
237
            mocked_response = mock.Mock()
238
            mocked_response.json.return_value = {'err': 0, 'err_desc': None, 'data': 'gateway response'}
239
            mocked_post.return_value = mocked_response
240
            broadcast.send()
241
            assert mocked_post.call_args[0][0] == 'http://sms.gateway'
242
            assert mocked_post.call_args[1]['json']['from'] == 'Corbo'
243
            assert isinstance(mocked_post.call_args[1]['json']['to'], list)
244
            assert broadcast.delivery_count == 3
tests/test_emailing.py
1
import pytest
2
from uuid import uuid4
3
import os
4
import re
5
import urllib
6
import mock
7

  
8
from django.core.urlresolvers import reverse
9
from django.core import mail, signing
10
from django.utils import timezone
11
from django.core.files.storage import DefaultStorage
12
from django.utils.text import slugify
13

  
14
from corbo.models import Category, Announce, Subscription, Broadcast
15
from corbo.models import channel_choices
16

  
17
pytestmark = pytest.mark.django_db
18

  
19
CATEGORIES = (u'Alerts', u'News')
20

  
21

  
22
@pytest.fixture
23
def categories():
24
    categories = []
25
    for category in CATEGORIES:
26
        c, created = Category.objects.get_or_create(name=category, slug=slugify(category))
27
        categories.append(c)
28
    return categories
29

  
30

  
31
@pytest.fixture
32
def announces():
33
    announces = []
34
    for category in Category.objects.all():
35
        a = Announce.objects.create(category=category, title='Announce 1',
36
                                    publication_time=timezone.now(),
37
                                    text='<h2>Announce 1</h2>')
38
        Broadcast.objects.create(announce=a)
39
        announces.append(a)
40
        a = Announce.objects.create(category=category, title='Announce 2',
41
                                    publication_time=timezone.now(),
42
                                    text='<h2>Announce 2</h2>')
43
        Broadcast.objects.create(announce=a)
44
        announces.append(a)
45
    return announces
46

  
47

  
48
def test_emailing_with_no_subscriptions(app, categories, announces, mailoutbox):
49
    for announce in announces:
50
        broadcast = Broadcast.objects.get(announce=announce)
51
        broadcast.send()
52
        assert not broadcast.delivery_count
53
    assert not len(mailoutbox)
54

  
55

  
56
def test_send_email(app, categories, announces, mailoutbox):
57
    for category in categories:
58
        uuid = uuid4()
59
        Subscription.objects.create(category=category,
60
                                    identifier='%s@example.net' % uuid, uuid=uuid)
61
    for i, announce in enumerate(announces):
62
        broadcast = Broadcast.objects.get(announce=announce)
63
        broadcast.send()
64
        assert broadcast.delivery_count
65
        assert len(mailoutbox) == i+1
66

  
67

  
68
def test_check_inline_css(app, categories, announces, mailoutbox):
69
    total_sent = 0
70
    for i, announce in enumerate(announces):
71
        announce.text = '<style type="text/css">h2 {color: #F00}</style>' + announce.text
72
        announce.save()
73
        uuid = uuid4()
74
        Subscription.objects.create(category=announce.category,
75
                                    identifier='%s@example.net' % uuid, uuid=uuid)
76
        broadcast = Broadcast.objects.get(announce=announce)
77
        broadcast.send()
78
        assert broadcast.delivery_count
79
        assert len(mailoutbox) == total_sent + broadcast.delivery_count
80
        total_sent += broadcast.delivery_count
81
        assert 'h2 style="color:#F00"' in mailoutbox[i].html
82

  
83

  
84
@mock.patch('emails.utils.requests.get')
85
def test_check_inline_images(mocked_get, app, categories, announces, mailoutbox):
86
    storage = DefaultStorage()
87
    media_path = os.path.join(os.path.dirname(__file__), 'media')
88
    image_name = 'logo.png'
89
    image_name = storage.save(image_name, file(os.path.join(media_path, image_name)))
90
    total_sent = 0
91
    for i, announce in enumerate(announces):
92
        img_src = "/media/%s" % image_name
93
        announce.text = announce.text + '<img src="%s" />' % img_src
94
        announce.save()
95
        uuid = uuid4()
96
        Subscription.objects.create(category=announce.category,
97
                                    identifier='%s@example.net' % uuid, uuid=uuid)
98
        broadcast = Broadcast.objects.get(announce=announce)
99
        mocked_get.return_value = mock.Mock(status_code=200,
100
                                            headers={'content-type': 'image/png'},
101
                                            content=storage.open(image_name).read())
102
        broadcast.send()
103
        assert broadcast.delivery_count
104

  
105
        assert len(mailoutbox) == total_sent + broadcast.delivery_count
106
        attachments = [a['filename'] for a in mailoutbox[0].attachments.as_dict()]
107
        assert image_name in attachments
108
        assert 'cid:%s' % image_name in mail.outbox[0].html_body
109
        assert 'cid:%s' % image_name in mail.outbox[0].text_body
110
        total_sent += broadcast.delivery_count
111
    storage.delete(image_name)
112

  
113

  
114
def test_unsubscription_link(app, categories, announces, custom_mailoutbox):
115
    unsubscription_link_sentinel = ''
116
    subscriptions_number = 3
117
    scheme = 'mailto:'
118
    for category in categories:
119
        for i in xrange(subscriptions_number):
120
            uuid = uuid4()
121
            uri = scheme + '%s@example.com' % uuid
122
            Subscription.objects.create(category=category, identifier=uri, uuid=str(uuid))
123

  
124
        for i, announce in enumerate(announces):
125
            if announce.category != category:
126
                continue
127
            broadcast = Broadcast.objects.get(announce=announce)
128
            broadcast.send()
129
            assert broadcast.delivery_count
130
            assert len(mail.outbox) == (i+1)*subscriptions_number
131
            assert mail.outbox[i*subscriptions_number].subject == announce.title
132

  
133
            for counter, destination in enumerate(category.subscription_set.all()):
134
                index = i*subscriptions_number+counter
135
                signature = urllib.unquote(re.findall('/unsubscribe/(.*)"', mail.outbox[index].html)[0])
136
                unsubscription_link = reverse('unsubscribe', kwargs={'unsubscription_token': signature})
137
                assert mail.outbox[index]._headers['List-Unsubscribe'] == '<http://localhost%s>' % unsubscription_link
138
                assert unsubscription_link in mail.outbox[index].html
139
                assert unsubscription_link in mail.outbox[index].text
140
                assert unsubscription_link_sentinel != unsubscription_link
141
                assert signing.loads(signature) == {
142
                    'category': announce.category.pk, 'identifier': destination.identifier}
143
                unsubscription_link_sentinel = unsubscription_link
144

  
145
                # make sure the uri schema is not in the page
146
                resp = app.get(unsubscription_link)
147
                assert scheme not in resp.content

Formats disponibles : Unified diff