Projet

Général

Profil

« Précédent | Suivant » 

Révision e93ea142

Ajouté par Serghei Mihai (congés, retour 15/05) il y a presque 8 ans

api: newsletters retrieval endpoint (#10794)

Voir les différences:

corbo/api_urls.py
1
# corbo - Announces Manager
2
# Copyright (C) 2016 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.conf.urls import patterns, include, url
18

  
19
from .api_views import NewslettersView
20

  
21
urlpatterns = patterns('',
22
            url(r'^newsletters/', NewslettersView.as_view(), name='newsletters'),
23
)
corbo/api_views.py
1
# corbo - Announces Manager
2
# Copyright (C) 2016 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from rest_framework.views import APIView
18
from rest_framework.response import Response
19

  
20
from .models import Category, Subscription, channel_choices
21

  
22

  
23
class NewslettersView(APIView):
24

  
25
    def get(self, request):
26
        newsletters = []
27
        transports = [{'id': identifier, 'text': name} for identifier, name in channel_choices]
28
        for c in Category.objects.all():
29
            newsletter = {'id': str(c.pk), 'text': c.name,
30
                          'transports': transports}
31
            newsletters.append(newsletter)
32
        return Response({'data': newsletters})
corbo/channels.py
1
from django.utils.translation import ugettext_lazy as _
2

  
3
def get_channel_choices(include=[], exclude=[]):
4
    for channel in HomepageChannel, SMSChannel, EmailChannel:
5
        if include and channel.identifier not in include:
6
            continue
7
        if exclude and channel.identifier in exclude:
8
            continue
9
        for identifier, display_name in channel.get_choices():
10
            yield (identifier, display_name)
11

  
12
class HomepageChannel(object):
13
    identifier = 'homepage'
14

  
15
    @classmethod
16
    def get_choices(self):
17
        return (('homepage', _('Homepage')),)
18

  
19
class SMSChannel(object):
20

  
21
    @classmethod
22
    def get_choices(self):
23
        return (('sms', _('SMS')),)
24

  
25
    def send(self, announce):
26
        pass
27

  
28
class EmailChannel(object):
29
    identifier = 'email'
30

  
31
    @classmethod
32
    def get_choices(self):
33
        return (('email', _('Email')),)
34

  
35
    def send(self, announce):
36
        pass
corbo/forms.py
1 1
from django import forms
2 2
from django.utils.translation import ugettext_lazy as _
3 3

  
4
from .models import Announce, Category, Broadcast
5
from .channels import get_channel_choices
4
from .models import Announce, Category, Broadcast, channel_choices
5

  
6 6

  
7 7
class AnnounceForm(forms.ModelForm):
8 8
    transport_channel = forms.MultipleChoiceField(required=False,
9
                                                  choices=get_channel_choices(),
9
                                                  choices=channel_choices,
10 10
                                                  widget=forms.CheckboxSelectMultiple())
11 11

  
12 12
    class Meta:
corbo/models.py
5 5

  
6 6
from ckeditor.fields import RichTextField
7 7

  
8
import channels
8
channel_choices = (
9
    ('mailto', _('Email')),
10
    ('homepage', _('Homepage'))
11
)
9 12

  
10 13
class Category(models.Model):
11 14
    name = models.CharField(max_length=64, blank=False, null=False)
......
51 54
class Broadcast(models.Model):
52 55
    announce = models.ForeignKey(Announce, verbose_name=_('announce'))
53 56
    channel = models.CharField(_('channel'), max_length=32,
54
            choices=channels.get_channel_choices(), blank=False)
57
            choices=channel_choices, blank=False)
55 58
    time = models.DateTimeField(_('sent time'), auto_now_add=True)
56 59
    result = models.TextField(_('result'), blank=True)
57 60

  
corbo/settings.py
41 41
    'django.contrib.sessions',
42 42
    'django.contrib.messages',
43 43
    'django.contrib.staticfiles',
44
    'rest_framework',
44 45
)
45 46

  
46 47
MIDDLEWARE_CLASSES = (
corbo/transports.py
1
import logging
2
import smtplib
3
import re
4
try:
5
    import simplejson as json
6
except:
7
    import json
8

  
9
import requests
10

  
11

  
12
from django.utils.importlib import import_module
13
from django.core.mail import EmailMessage
14
from django.template.loader import select_template
15
from django.template import Context
16
from django.utils.translation import ugettext_lazy as _
17

  
18

  
19
import app_settings
20
import models
21

  
22
logger = logging.getLogger()
23

  
24

  
25
def get_transport_choices(include=[], exclude=[]):
26
    for transport in get_transports():
27
        if include and transport.identifier not in include:
28
            continue
29
        if exclude and transport.identifier in exclude:
30
            continue
31
        for identifier, display_name in transport.get_choices():
32
            yield (identifier, display_name)
33

  
34

  
35
def get_transport(identifier):
36
    transports = get_transports()
37
    for transport in transports:
38
        if identifier == transport.identifier:
39
            return transport
40
    return None
41

  
42

  
43
__TRANSPORTS = None
44

  
45

  
46
def get_transports():
47
    global __TRANSPORTS
48

  
49
    if __TRANSPORTS is None:
50
        transports = []
51
        for class_path in app_settings.transport_modes:
52
            if not isinstance(class_path, basestring):
53
                class_path, kwargs = class_path
54
            else:
55
                kwargs = {}
56
            module_path, class_name = class_path.rsplit('.', 1)
57
            try:
58
                module = import_module(module_path)
59
                transports.append(getattr(module, class_name)(**kwargs))
60
            except (ImportError, AttributeError), e:
61
                raise ImportError('Unable to load transport class %s' % class_path, e)
62
        __TRANSPORTS = transports
63
    return __TRANSPORTS
64

  
65

  
66
def get_template_list(template_list, **kwargs):
67
    '''Customize a template list given an announce category'''
68
    for template in template_list:
69
        yield template.format(**kwargs)
70

  
71

  
72
def get_template(template_list, **kwargs):
73
    template_list = get_template_list(template_list, **kwargs)
74
    return select_template(template_list)
75

  
76

  
77
class HomepageTransport(object):
78
    identifier = 'homepage'
79

  
80
    def get_choices(self):
81
        return (('homepage', _('Homepage')),)
82

  
83
    def get_identifier_from_subscription(self, subscription):
84
        return u'homepage'
85

  
86

  
87
class SMSTransport(object):
88
    body_template_list = [
89
        'portail_citoyen_announces/{identifier}/body_{category}.txt',
90
        'portail_citoyen_announces/{identifier}/body.txt',
91
        'portail_citoyen_announces/body_{category}.txt',
92
        'portail_citoyen_announces/body.txt',
93
    ]
94
    mobile_re = re.compile('^0[67][0-9]{8}$')
95

  
96
    def __init__(self, url, from_mobile, login=None, password=None, identifier='sms', name=_('SMS')):
97
        self.url = url
98
        self.from_mobile = from_mobile
99
        self.login = login
100
        self.password = password
101
        self.identifier = identifier
102
        self.name = name
103

  
104
    def get_choices(self):
105
        return ((self.identifier, self.name),)
106

  
107
    def get_subscriptions(self, category):
108
        return models.Subscription.objects.filter(category=category,
109
                transport=self.identifier)
110

  
111
    def get_sms(self, category):
112
        qs = self.get_subscriptions(category)
113
        for subscription in qs:
114
            sms = ''
115
            if subscription.identifier:
116
                sms = subscription.identifier
117
            elif subscription.user:
118
                sms = subscription.user.mobile
119
            if self.mobile_re.match(sms):
120
                yield sms
121

  
122
    def send(self, announce):
123
        category = announce.category
124
        site = category.site
125
        body_template = get_template(self.body_template_list,
126
                category=category.identifier, identifier=self.identifier)
127
        ctx = Context({ 'announce': announce, 'site': site, 'category': category })
128
        body = body_template.render(ctx)
129
        sms = list(self.get_sms(category))
130
        logger.info(u'sending announce %(announce)s through %(mode)s to %(count)s emails',
131
                dict(announce=announce, mode=self.identifier, count=len(sms)))
132
        try:
133
            payload = {
134
                    'message': body,
135
                    'from': self.from_mobile,
136
                    'to': list(sms),
137
            }
138
            response = requests.post(self.url, data=json.dumps(payload))
139
            json_response = response.json()
140
            if json_response['err'] != 0:
141
                msg = u'unable to send announce "%s" on site "%s": %s' % (announce,
142
                    site, json_response)
143
                logger.error(msg)
144
            else:
145
                logger.info('announce %(announce)s sent succesfully',
146
                        dict(announce=announce))
147
                msg = u'ok'
148
        except smtplib.SMTPException, e:
149
            msg = u'unable to send announce "%s" on site "%s": %s' % (announce,
150
                site, e)
151
            logger.error(msg)
152
        except Exception, e:
153
            msg = u'unable to send announce "%s" on site "%s": %s' % (announce,
154
                site, e)
155
            logger.exception(msg)
156
        models.Sent.objects.create(
157
            announce=announce,
158
            transport=self.identifier,
159
            result=msg)
160

  
161
    def get_identifier_from_subscription(self, subscription):
162
        if subscription.user:
163
            return subscription.user.mobile
164
        return subscription.identifier
165

  
166
class EmailTransport(object):
167
    identifier = 'email'
168

  
169
    subject_template_list = [
170
        'portail_citoyen_announces/email/subject_{category}.txt',
171
        'portail_citoyen_announces/email/subject.txt',
172
        'portail_citoyen_announces/subject_{category}.txt',
173
        'portail_citoyen_announces/subject.txt',
174
    ]
175

  
176
    body_template_list = [
177
        'portail_citoyen_announces/email/body_{category}.txt',
178
        'portail_citoyen_announces/email/body.txt',
179
        'portail_citoyen_announces/body_{category}.txt',
180
        'portail_citoyen_announces/body.txt',
181
    ]
182

  
183
    def get_choices(self):
184
        return (('email', _('Email')),)
185

  
186
    def get_subscriptions(self, category):
187
        return models.Subscription.objects.filter(category=category,
188
                transport=self.identifier)
189

  
190
    def get_emails(self, category):
191
        qs = self.get_subscriptions(category)
192
        for subscription in qs:
193
            email = ''
194
            if subscription.identifier:
195
                email = subscription.identifier
196
            elif subscription.user:
197
                email = subscription.user.email
198
            yield email
199

  
200
    def send(self, announce):
201
        category = announce.category
202
        site = category.site
203
        subject_template = get_template(self.subject_template_list,
204
                category=category.identifier, identifier=self.identifier)
205
        body_template = get_template(self.body_template_list,
206
                category=category.identifier, identifier=self.identifier)
207
        ctx = Context({ 'announce': announce, 'site': site, 'category': category })
208
        subject = subject_template.render(ctx).replace('\r', '').replace('\n', '')
209
        body = body_template.render(ctx)
210
        emails = list(self.get_emails(category))
211
        logger.info(u'sending announce %(announce)s through %(mode)s to %(count)s emails',
212
                dict(announce=announce, mode=self.identifier, count=len(emails)))
213
        try:
214
            message = EmailMessage(subject=subject, 
215
                    body=body, 
216
                    from_email=app_settings.default_from,
217
                    bcc=emails)
218
            message.send()
219
        except smtplib.SMTPException, e:
220
            msg = u'unable to send announce "%s" on site "%s": %s' % (announce,
221
                site, e)
222
            logger.error(msg)
223
        except Exception, e:
224
            msg = u'unable to send announce "%s" on site "%s": %s' % (announce,
225
                site, e)
226
            logger.exception(msg)
227
        else:
228
            logger.info('announce %(announce)s sent succesfully',
229
                    dict(announce=announce))
230
            msg = u'ok'
231
        models.Sent.objects.create(
232
            announce=announce,
233
            transport=self.identifier,
234
            result=msg)
235

  
236
    def get_identifier_from_subscription(self, subscription):
237
        if subscription.user:
238
            return subscription.user.email
239
        return subscription.identifier
corbo/urls.py
8 8
from .views import homepage, atom
9 9

  
10 10
from manage_urls import urlpatterns as manage_urls
11
from api_urls import urlpatterns as api_urls
11 12

  
12 13
urlpatterns = patterns('',
13 14
    url(r'^$', homepage, name='home'),
......
15 16
    url(r'^manage/', decorated_includes(manager_required,
16 17
                    include(manage_urls))),
17 18
    url(r'^ckeditor/', include('ckeditor.urls')),
18
    url(r'^admin/', include(admin.site.urls))
19
    url(r'^admin/', include(admin.site.urls)),
20
    url(r'^api/', include(api_urls))
19 21
)
20 22

  
21 23
if 'mellon' in settings.INSTALLED_APPS:
jenkins.sh
1
#!/bin/sh
2

  
3
set -e
4

  
5
rm -f coverage.xml
6
rm -f test_results.xml
7

  
8
pip install --upgrade tox
9
pip install --upgrade pylint pylint-django
10
tox -r
11
test -f pylint.out && cp pylint.out pylint.out.prev
12
(pylint -f parseable --rcfile /var/lib/jenkins/pylint.django.rc corbo/ | tee pylint.out) || /bin/true
13
test -f pylint.out.prev && (diff pylint.out.prev pylint.out | grep '^[><]' | grep .py) || /bin/true
requirements.txt
1 1
Django>=1.7, <1.8
2 2
django-ckeditor<4.5.3
3
djangorestframework
3 4
-e git+http://repos.entrouvert.org/gadjo.git/#egg=gadjo
setup.py
94 94
        'Programming Language :: Python :: 2',
95 95
    ],
96 96
    install_requires=['django>=1.7, <1.8',
97
        'django-ckeditor<4.5.3'
97
        'django-ckeditor<4.5.3',
98
        'djangorestframework',
98 99
        'gadjo'
99 100
        ],
100 101
    zip_safe=False,
tests/conftest.py
1
import pytest
2
import django_webtest
3

  
4
@pytest.fixture
5
def app(request):
6
    wtm = django_webtest.WebTestMixin()
7
    wtm._patch_settings()
8
    request.addfinalizer(wtm._unpatch_settings)
9
    return django_webtest.DjangoTestApp()
tests/test_api.py
1
import pytest
2
import json
3

  
4

  
5
from django.core.urlresolvers import reverse
6

  
7
from corbo.models import Category, Announce, Broadcast
8

  
9
pytestmark = pytest.mark.django_db
10

  
11
CATEGORIES = ('Alerts', 'News')
12

  
13

  
14
@pytest.fixture
15
def categories():
16
    categories = []
17
    for category in CATEGORIES:
18
        c, created = Category.objects.get_or_create(name=category)
19
        categories.append(c)
20
    return categories
21

  
22
@pytest.fixture
23
def announces():
24
    announces = []
25
    for category in Category.objects.all():
26
        a = Announce.objects.create(category=category, title='By email')
27
        Broadcast.objects.create(announce=a, channel='mailto')
28
        announces.append(a)
29
        a = Announce.objects.create(category=category, title='On homepage')
30
        Broadcast.objects.create(announce=a, channel='homepage')
31
        announces.append(a)
32
    return announces
33

  
34

  
35
def test_get_newsletters(app, categories, announces):
36
    resp = app.get(reverse('newsletters'), status=200)
37
    data = resp.json
38
    assert data['data']
39
    for category in data['data']:
40
        assert 'id' in category
41
        assert 'text' in category
42
        assert category['text'] in CATEGORIES
43
        assert 'transports' in category
44
        assert category['transports'] == [{'id': 'mailto', 'text': 'Email'},
45
                                          {'id': 'homepage', 'text': 'Homepage'}
46
                                          ]
tox.ini
1
[tox]
2
envlist = coverage-{django17,django18}
3

  
4
[testenv]
5
usedevelop =
6
  coverage: True
7
setenv =
8
  DJANGO_SETTINGS_MODULE=corbo.settings
9
  coverage: COVERAGE=--junitxml=test_results.xml --cov-report xml --cov=corbo/ --cov-config .coveragerc
10
deps =
11
  django17: django>1.7,<1.8
12
  django18: django>=1.8,<1.9
13
  pytest-cov
14
  pytest-django
15
  pytest
16
  pytest-capturelog
17
  django-webtest
18
  django-ckeditor<4.5.3
19
  djangorestframework
20
  pylint==1.4.0
21
  astroid==1.3.2
22
commands =
23
  py.test {env:COVERAGE:} {posargs:tests/}

Formats disponibles : Unified diff