From c0214b3910d19b68e6158079f1654b03af9de7b1 Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Fri, 6 May 2016 17:21:56 +0200 Subject: [PATCH] announces send command (#10805) --- corbo/models.py | 39 +++++++++++++++++++++ corbo/settings.py | 3 ++ requirements.txt | 3 ++ setup.py | 5 ++- tests/media/logo.png | Bin 0 -> 7876 bytes tests/test_emailing.py | 93 +++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 tests/media/logo.png create mode 100644 tests/test_emailing.py diff --git a/corbo/models.py b/corbo/models.py index 3c22ec9..533b687 100644 --- a/corbo/models.py +++ b/corbo/models.py @@ -1,6 +1,13 @@ +from datetime import datetime +import logging +from html2text import HTML2Text +from emails.django import Message +from lxml.etree import HTML as HTMLTree + from django.utils import timezone from django.conf import settings from django.db import models +from django.core.files.storage import DefaultStorage from django.utils.translation import ugettext_lazy as _ from ckeditor.fields import RichTextField @@ -10,6 +17,8 @@ channel_choices = ( ('homepage', _('Homepage')) ) +logger = logging.getLogger(__name__) + class Category(models.Model): name = models.CharField(max_length=64, blank=False, null=False) ctime = models.DateTimeField(auto_now_add=True) @@ -62,6 +71,36 @@ class Broadcast(models.Model): id=self.announce.id, time=self.deliver_time) return u'announce {id} to deliver'.format(id=self.announce.id) + def send(self): + subscriptions = self.announce.category.subscription_set.all() + total_sent = 0 + handler = HTML2Text() + m = Message(html=self.announce.text, subject=self.announce.title, + text=handler.handle(self.announce.text), + mail_from=settings.DEFAULT_FROM_EMAIL) + html_tree = HTMLTree(self.announce.text) + storage = DefaultStorage() + for img in html_tree.xpath('//img/@src'): + img_path = img.lstrip(storage.base_url) + m.attach(filename=img, data=storage.open(img_path)) + m.attachments[img].is_inline = True + m.transformer.synchronize_inline_images() + m.transformer.load_and_transform() + m.transformer.save() + for s in subscriptions: + if not s.identifier: + continue + sent = m.send(to=s.identifier) + if sent: + total_sent += 1 + logger.info('Announce "%s" sent to %s', self.announce.title, s.identifier) + else: + logger.warning('Error occured while sending announce "%s" to %s.', + self.announce.title, s.identifier) + self.result = total_sent + self.deliver_time = timezone.now() + self.save() + class Meta: verbose_name = _('sent') ordering = ('-deliver_time',) diff --git a/corbo/settings.py b/corbo/settings.py index 654c98c..a70dacc 100644 --- a/corbo/settings.py +++ b/corbo/settings.py @@ -104,6 +104,9 @@ RSS_DESCRIPTION = '' RSS_LINK = '' RSS_LINK_TEMPLATE = '/#announce{0}' +# default mass emails expeditor +CORBO_DEFAULT_FROM_EMAIL = 'webmaster@localhost' + # django-mellon settings MELLON_ATTRIBUTE_MAPPING = { 'username': '{attributes[username][0]}', diff --git a/requirements.txt b/requirements.txt index 2f6bdfc..2d0946c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ Django>=1.7, <1.8 django-ckeditor<4.5.3 djangorestframework +html2text +emails -e git+http://repos.entrouvert.org/gadjo.git/#egg=gadjo + diff --git a/setup.py b/setup.py index e59a798..2254dba 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,10 @@ setup( install_requires=['django>=1.7, <1.8', 'django-ckeditor<4.5.3', 'djangorestframework', - 'gadjo' + 'html2text', + 'gadjo', + 'emails', + 'lxml', ], zip_safe=False, cmdclass={ diff --git a/tests/media/logo.png b/tests/media/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fb954dd0ad98bcc8b9329c5e8e4c00b0dc2da4e2 GIT binary patch literal 7876 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 id7Y_W+b4xk9sGa!v(gm_8^M4800007 literal 0 HcmV?d00001 diff --git a/tests/test_emailing.py b/tests/test_emailing.py new file mode 100644 index 0000000..e7a134d --- /dev/null +++ b/tests/test_emailing.py @@ -0,0 +1,93 @@ +import pytest +import json +from uuid import uuid4 +import os + +from django.core.urlresolvers import reverse +from django.utils.http import urlencode +from django.core import mail +from django.utils import timezone +from django.core.files.storage import DefaultStorage + +from corbo.models import Category, Announce, Subscription, Broadcast +from corbo.models import channel_choices + +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='Announce 1', + publication_time=timezone.now(), + text='

Announce 1

') + Broadcast.objects.create(announce=a) + announces.append(a) + a = Announce.objects.create(category=category, title='Announce 2', + publication_time=timezone.now(), + text='

Announce 2

') + Broadcast.objects.create(announce=a) + announces.append(a) + return announces + +def test_emailing_with_no_subscriptions(app, categories, announces): + for announce in announces: + broadcast = Broadcast.objects.get(announce=announce) + broadcast.send() + assert not broadcast.result + assert not mail.outbox + +def test_send_email(app, categories, announces): + for category in categories: + uuid = uuid4() + s = Subscription.objects.create(category=category, + identifier='%s@example.net' % uuid, uuid=uuid) + for announce in announces: + broadcast= Broadcast.objects.get(announce=announce) + broadcast.send() + assert broadcast.result + assert mail.outbox + +def test_check_inline_css(app, categories, announces): + for announce in announces: + announce.text = '' + announce.text + announce.save() + uuid = uuid4() + s = Subscription.objects.create(category=announce.category, + identifier='%s@example.net' % uuid, uuid=uuid) + broadcast = Broadcast.objects.get(announce=announce) + broadcast.send() + assert broadcast.result + assert mail.outbox + assert 'h2 style="color:#F00"' in mail.outbox[0].html + mail.outbox = [] + +def test_check_inline_images(app, categories, announces): + storage = DefaultStorage() + media_path = os.path.join(os.path.dirname(__file__), 'media') + image_name = 'logo.png' + storage.save(image_name, file(os.path.join(media_path, image_name))) + for announce in announces: + announce.text = announce.text + '' % image_name + announce.save() + uuid = uuid4() + s = Subscription.objects.create(category=announce.category, + identifier='%s@example.net' % uuid, uuid=uuid) + broadcast = Broadcast.objects.get(announce=announce) + broadcast.send() + assert broadcast.result + assert mail.outbox + assert storage.url(image_name) in mail.outbox[0].attachments.keys() + mail.outbox = [] + storage.delete(image_name) -- 2.8.1