Revision e93ea142
Added by Serghei Mihai over 8 years ago
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/} |
Also available in: Unified diff
api: newsletters retrieval endpoint (#10794)