api: newsletters retrieval endpoint (#10794)

This commit is contained in:
Serghei Mihai 2016-05-02 15:24:29 +02:00
parent 0296e37098
commit e93ea1421d
14 changed files with 161 additions and 282 deletions

23
corbo/api_urls.py Normal file
View File

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

32
corbo/api_views.py Normal file
View File

@ -0,0 +1,32 @@
# corbo - Announces Manager
# Copyright (C) 2016 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Category, Subscription, channel_choices
class NewslettersView(APIView):
def get(self, request):
newsletters = []
transports = [{'id': identifier, 'text': name} for identifier, name in channel_choices]
for c in Category.objects.all():
newsletter = {'id': str(c.pk), 'text': c.name,
'transports': transports}
newsletters.append(newsletter)
return Response({'data': newsletters})

View File

@ -1,36 +0,0 @@
from django.utils.translation import ugettext_lazy as _
def get_channel_choices(include=[], exclude=[]):
for channel in HomepageChannel, SMSChannel, EmailChannel:
if include and channel.identifier not in include:
continue
if exclude and channel.identifier in exclude:
continue
for identifier, display_name in channel.get_choices():
yield (identifier, display_name)
class HomepageChannel(object):
identifier = 'homepage'
@classmethod
def get_choices(self):
return (('homepage', _('Homepage')),)
class SMSChannel(object):
@classmethod
def get_choices(self):
return (('sms', _('SMS')),)
def send(self, announce):
pass
class EmailChannel(object):
identifier = 'email'
@classmethod
def get_choices(self):
return (('email', _('Email')),)
def send(self, announce):
pass

View File

@ -1,12 +1,12 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from .models import Announce, Category, Broadcast
from .channels import get_channel_choices
from .models import Announce, Category, Broadcast, channel_choices
class AnnounceForm(forms.ModelForm):
transport_channel = forms.MultipleChoiceField(required=False,
choices=get_channel_choices(),
choices=channel_choices,
widget=forms.CheckboxSelectMultiple())
class Meta:

View File

@ -5,7 +5,10 @@ from django.utils.translation import ugettext_lazy as _
from ckeditor.fields import RichTextField
import channels
channel_choices = (
('mailto', _('Email')),
('homepage', _('Homepage'))
)
class Category(models.Model):
name = models.CharField(max_length=64, blank=False, null=False)
@ -51,7 +54,7 @@ class Announce(models.Model):
class Broadcast(models.Model):
announce = models.ForeignKey(Announce, verbose_name=_('announce'))
channel = models.CharField(_('channel'), max_length=32,
choices=channels.get_channel_choices(), blank=False)
choices=channel_choices, blank=False)
time = models.DateTimeField(_('sent time'), auto_now_add=True)
result = models.TextField(_('result'), blank=True)

View File

@ -41,6 +41,7 @@ INSTALLED_APPS = (
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
)
MIDDLEWARE_CLASSES = (

View File

@ -1,239 +0,0 @@
import logging
import smtplib
import re
try:
import simplejson as json
except:
import json
import requests
from django.utils.importlib import import_module
from django.core.mail import EmailMessage
from django.template.loader import select_template
from django.template import Context
from django.utils.translation import ugettext_lazy as _
import app_settings
import models
logger = logging.getLogger()
def get_transport_choices(include=[], exclude=[]):
for transport in get_transports():
if include and transport.identifier not in include:
continue
if exclude and transport.identifier in exclude:
continue
for identifier, display_name in transport.get_choices():
yield (identifier, display_name)
def get_transport(identifier):
transports = get_transports()
for transport in transports:
if identifier == transport.identifier:
return transport
return None
__TRANSPORTS = None
def get_transports():
global __TRANSPORTS
if __TRANSPORTS is None:
transports = []
for class_path in app_settings.transport_modes:
if not isinstance(class_path, basestring):
class_path, kwargs = class_path
else:
kwargs = {}
module_path, class_name = class_path.rsplit('.', 1)
try:
module = import_module(module_path)
transports.append(getattr(module, class_name)(**kwargs))
except (ImportError, AttributeError), e:
raise ImportError('Unable to load transport class %s' % class_path, e)
__TRANSPORTS = transports
return __TRANSPORTS
def get_template_list(template_list, **kwargs):
'''Customize a template list given an announce category'''
for template in template_list:
yield template.format(**kwargs)
def get_template(template_list, **kwargs):
template_list = get_template_list(template_list, **kwargs)
return select_template(template_list)
class HomepageTransport(object):
identifier = 'homepage'
def get_choices(self):
return (('homepage', _('Homepage')),)
def get_identifier_from_subscription(self, subscription):
return u'homepage'
class SMSTransport(object):
body_template_list = [
'portail_citoyen_announces/{identifier}/body_{category}.txt',
'portail_citoyen_announces/{identifier}/body.txt',
'portail_citoyen_announces/body_{category}.txt',
'portail_citoyen_announces/body.txt',
]
mobile_re = re.compile('^0[67][0-9]{8}$')
def __init__(self, url, from_mobile, login=None, password=None, identifier='sms', name=_('SMS')):
self.url = url
self.from_mobile = from_mobile
self.login = login
self.password = password
self.identifier = identifier
self.name = name
def get_choices(self):
return ((self.identifier, self.name),)
def get_subscriptions(self, category):
return models.Subscription.objects.filter(category=category,
transport=self.identifier)
def get_sms(self, category):
qs = self.get_subscriptions(category)
for subscription in qs:
sms = ''
if subscription.identifier:
sms = subscription.identifier
elif subscription.user:
sms = subscription.user.mobile
if self.mobile_re.match(sms):
yield sms
def send(self, announce):
category = announce.category
site = category.site
body_template = get_template(self.body_template_list,
category=category.identifier, identifier=self.identifier)
ctx = Context({ 'announce': announce, 'site': site, 'category': category })
body = body_template.render(ctx)
sms = list(self.get_sms(category))
logger.info(u'sending announce %(announce)s through %(mode)s to %(count)s emails',
dict(announce=announce, mode=self.identifier, count=len(sms)))
try:
payload = {
'message': body,
'from': self.from_mobile,
'to': list(sms),
}
response = requests.post(self.url, data=json.dumps(payload))
json_response = response.json()
if json_response['err'] != 0:
msg = u'unable to send announce "%s" on site "%s": %s' % (announce,
site, json_response)
logger.error(msg)
else:
logger.info('announce %(announce)s sent succesfully',
dict(announce=announce))
msg = u'ok'
except smtplib.SMTPException, e:
msg = u'unable to send announce "%s" on site "%s": %s' % (announce,
site, e)
logger.error(msg)
except Exception, e:
msg = u'unable to send announce "%s" on site "%s": %s' % (announce,
site, e)
logger.exception(msg)
models.Sent.objects.create(
announce=announce,
transport=self.identifier,
result=msg)
def get_identifier_from_subscription(self, subscription):
if subscription.user:
return subscription.user.mobile
return subscription.identifier
class EmailTransport(object):
identifier = 'email'
subject_template_list = [
'portail_citoyen_announces/email/subject_{category}.txt',
'portail_citoyen_announces/email/subject.txt',
'portail_citoyen_announces/subject_{category}.txt',
'portail_citoyen_announces/subject.txt',
]
body_template_list = [
'portail_citoyen_announces/email/body_{category}.txt',
'portail_citoyen_announces/email/body.txt',
'portail_citoyen_announces/body_{category}.txt',
'portail_citoyen_announces/body.txt',
]
def get_choices(self):
return (('email', _('Email')),)
def get_subscriptions(self, category):
return models.Subscription.objects.filter(category=category,
transport=self.identifier)
def get_emails(self, category):
qs = self.get_subscriptions(category)
for subscription in qs:
email = ''
if subscription.identifier:
email = subscription.identifier
elif subscription.user:
email = subscription.user.email
yield email
def send(self, announce):
category = announce.category
site = category.site
subject_template = get_template(self.subject_template_list,
category=category.identifier, identifier=self.identifier)
body_template = get_template(self.body_template_list,
category=category.identifier, identifier=self.identifier)
ctx = Context({ 'announce': announce, 'site': site, 'category': category })
subject = subject_template.render(ctx).replace('\r', '').replace('\n', '')
body = body_template.render(ctx)
emails = list(self.get_emails(category))
logger.info(u'sending announce %(announce)s through %(mode)s to %(count)s emails',
dict(announce=announce, mode=self.identifier, count=len(emails)))
try:
message = EmailMessage(subject=subject,
body=body,
from_email=app_settings.default_from,
bcc=emails)
message.send()
except smtplib.SMTPException, e:
msg = u'unable to send announce "%s" on site "%s": %s' % (announce,
site, e)
logger.error(msg)
except Exception, e:
msg = u'unable to send announce "%s" on site "%s": %s' % (announce,
site, e)
logger.exception(msg)
else:
logger.info('announce %(announce)s sent succesfully',
dict(announce=announce))
msg = u'ok'
models.Sent.objects.create(
announce=announce,
transport=self.identifier,
result=msg)
def get_identifier_from_subscription(self, subscription):
if subscription.user:
return subscription.user.email
return subscription.identifier

View File

@ -8,6 +8,7 @@ from .urls_utils import decorated_includes, manager_required
from .views import homepage, atom
from manage_urls import urlpatterns as manage_urls
from api_urls import urlpatterns as api_urls
urlpatterns = patterns('',
url(r'^$', homepage, name='home'),
@ -15,7 +16,8 @@ urlpatterns = patterns('',
url(r'^manage/', decorated_includes(manager_required,
include(manage_urls))),
url(r'^ckeditor/', include('ckeditor.urls')),
url(r'^admin/', include(admin.site.urls))
url(r'^admin/', include(admin.site.urls)),
url(r'^api/', include(api_urls))
)
if 'mellon' in settings.INSTALLED_APPS:

13
jenkins.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/sh
set -e
rm -f coverage.xml
rm -f test_results.xml
pip install --upgrade tox
pip install --upgrade pylint pylint-django
tox -r
test -f pylint.out && cp pylint.out pylint.out.prev
(pylint -f parseable --rcfile /var/lib/jenkins/pylint.django.rc corbo/ | tee pylint.out) || /bin/true
test -f pylint.out.prev && (diff pylint.out.prev pylint.out | grep '^[><]' | grep .py) || /bin/true

View File

@ -1,3 +1,4 @@
Django>=1.7, <1.8
django-ckeditor<4.5.3
djangorestframework
-e git+http://repos.entrouvert.org/gadjo.git/#egg=gadjo

View File

@ -94,7 +94,8 @@ setup(
'Programming Language :: Python :: 2',
],
install_requires=['django>=1.7, <1.8',
'django-ckeditor<4.5.3'
'django-ckeditor<4.5.3',
'djangorestframework',
'gadjo'
],
zip_safe=False,

9
tests/conftest.py Normal file
View File

@ -0,0 +1,9 @@
import pytest
import django_webtest
@pytest.fixture
def app(request):
wtm = django_webtest.WebTestMixin()
wtm._patch_settings()
request.addfinalizer(wtm._unpatch_settings)
return django_webtest.DjangoTestApp()

46
tests/test_api.py Normal file
View File

@ -0,0 +1,46 @@
import pytest
import json
from django.core.urlresolvers import reverse
from corbo.models import Category, Announce, Broadcast
pytestmark = pytest.mark.django_db
CATEGORIES = ('Alerts', 'News')
@pytest.fixture
def categories():
categories = []
for category in CATEGORIES:
c, created = Category.objects.get_or_create(name=category)
categories.append(c)
return categories
@pytest.fixture
def announces():
announces = []
for category in Category.objects.all():
a = Announce.objects.create(category=category, title='By email')
Broadcast.objects.create(announce=a, channel='mailto')
announces.append(a)
a = Announce.objects.create(category=category, title='On homepage')
Broadcast.objects.create(announce=a, channel='homepage')
announces.append(a)
return announces
def test_get_newsletters(app, categories, announces):
resp = app.get(reverse('newsletters'), status=200)
data = resp.json
assert data['data']
for category in data['data']:
assert 'id' in category
assert 'text' in category
assert category['text'] in CATEGORIES
assert 'transports' in category
assert category['transports'] == [{'id': 'mailto', 'text': 'Email'},
{'id': 'homepage', 'text': 'Homepage'}
]

23
tox.ini Normal file
View File

@ -0,0 +1,23 @@
[tox]
envlist = coverage-{django17,django18}
[testenv]
usedevelop =
coverage: True
setenv =
DJANGO_SETTINGS_MODULE=corbo.settings
coverage: COVERAGE=--junitxml=test_results.xml --cov-report xml --cov=corbo/ --cov-config .coveragerc
deps =
django17: django>1.7,<1.8
django18: django>=1.8,<1.9
pytest-cov
pytest-django
pytest
pytest-capturelog
django-webtest
django-ckeditor<4.5.3
djangorestframework
pylint==1.4.0
astroid==1.3.2
commands =
py.test {env:COVERAGE:} {posargs:tests/}