From 3710bcf9692126dae7973b0e50921104cfb49c95 Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Thu, 23 Jun 2016 18:20:15 +0200 Subject: [PATCH] unsubscribe link (#10795) --- corbo/models.py | 22 +++++++++++++------ corbo/static/css/corbo.css | 17 +++++++++++++++ corbo/templates/corbo/announce.html | 9 ++++++++ .../corbo/subscription_confirm_delete.html | 18 ++++++++++++++++ corbo/templates/corbo/unsubscription.html | 14 ++++++++++++ corbo/templates/corbo/unsubscription_done.html | 10 +++++++++ corbo/urls.py | 8 +++++-- corbo/views.py | 25 ++++++++++++++++++++++ tests/test_emailing.py | 25 +++++++++++++++++++++- 9 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 corbo/templates/corbo/announce.html create mode 100644 corbo/templates/corbo/subscription_confirm_delete.html create mode 100644 corbo/templates/corbo/unsubscription.html create mode 100644 corbo/templates/corbo/unsubscription_done.html diff --git a/corbo/models.py b/corbo/models.py index 533b687..ccf0b9c 100644 --- a/corbo/models.py +++ b/corbo/models.py @@ -9,6 +9,9 @@ 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 django.core import signing +from django.template import loader, Context +from django.core.urlresolvers import reverse from ckeditor.fields import RichTextField @@ -75,21 +78,28 @@ class Broadcast(models.Model): 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) + template = loader.get_template('corbo/announce.html') + m = Message(subject=self.announce.title, 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 + unsubscribe_token = signing.dumps({'category': self.announce.category.pk, + 'identifier': s.identifier}) + unsubscribe_link = reverse('unsubscribe', kwargs={'unsubscription_token': unsubscribe_token}) + message = template.render(Context({'unsubscribe_link': unsubscribe_link, + 'content': self.announce.text})) + m.html = message + m.transformer.load_and_transform() + m.transformer.synchronize_inline_images() + m.transformer.save() + handler.body_width = 0 + m.text = handler.handle(message) sent = m.send(to=s.identifier) if sent: total_sent += 1 diff --git a/corbo/static/css/corbo.css b/corbo/static/css/corbo.css index 1dad7dd..d3406f5 100644 --- a/corbo/static/css/corbo.css +++ b/corbo/static/css/corbo.css @@ -280,4 +280,21 @@ form ul li label { #id_transport_channel li, #id_transport_channel label { display: inline; +} + +.content { + width: 50%; + margin: auto; +} + +.unsubscription { + background: #e6db74; + border: 1px solid #aaa; + padding: 5px; +} + +.info { + background: #a6e22e; + border: 1px solid #aaa; + padding: 5px; } \ No newline at end of file diff --git a/corbo/templates/corbo/announce.html b/corbo/templates/corbo/announce.html new file mode 100644 index 0000000..39592e4 --- /dev/null +++ b/corbo/templates/corbo/announce.html @@ -0,0 +1,9 @@ +{% load i18n %} +
+ {{ content|safe }} +
+ diff --git a/corbo/templates/corbo/subscription_confirm_delete.html b/corbo/templates/corbo/subscription_confirm_delete.html new file mode 100644 index 0000000..f8ed871 --- /dev/null +++ b/corbo/templates/corbo/subscription_confirm_delete.html @@ -0,0 +1,18 @@ +{% extends "corbo/unsubscription.html" %} +{% load i18n %} + +{% block title %}{% blocktrans with object.category as category %} +Unsubscription from {{ category }} +{% endblocktrans %}{% endblock %} + +{% block content %} +
+{% blocktrans with object.category as category and object.identifier as identifier %} +Are you sure you want to unsubscribe {{ identifier }} from "{{ category }}"? +{% endblocktrans %} +
+ {% csrf_token %} + +
+
+{% endblock %} diff --git a/corbo/templates/corbo/unsubscription.html b/corbo/templates/corbo/unsubscription.html new file mode 100644 index 0000000..46adeeb --- /dev/null +++ b/corbo/templates/corbo/unsubscription.html @@ -0,0 +1,14 @@ +{% load static %} + + + + {% block title %}{% endblock %} + + + +
+ {% block content %} + {% endblock %} +
+ + diff --git a/corbo/templates/corbo/unsubscription_done.html b/corbo/templates/corbo/unsubscription_done.html new file mode 100644 index 0000000..6ea40b2 --- /dev/null +++ b/corbo/templates/corbo/unsubscription_done.html @@ -0,0 +1,10 @@ +{% extends "corbo/unsubscription.html" %} +{% load i18n %} + +{% block title %} +{% trans "Successfully unsubscription" %} +{% endblock %} + +{% block content %} +
{% trans "You were sucessfully unsubcribed." %}
+{% endblock %} diff --git a/corbo/urls.py b/corbo/urls.py index f8b074d..c52dfd4 100644 --- a/corbo/urls.py +++ b/corbo/urls.py @@ -5,7 +5,7 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib import admin from .urls_utils import decorated_includes, manager_required -from .views import homepage, atom +from .views import homepage, atom, unsubscribe, unsubscription_done from manage_urls import urlpatterns as manage_urls from api_urls import urlpatterns as api_urls @@ -17,7 +17,11 @@ urlpatterns = patterns('', include(manage_urls))), url(r'^ckeditor/', include('ckeditor.urls')), url(r'^admin/', include(admin.site.urls)), - url(r'^api/', include(api_urls)) + url(r'^api/', include(api_urls)), + url(r'^unsubscribe/done/$', unsubscription_done, + name='unsubscription_done'), + url(r'^unsubscribe/(?P[\w:-]+)$', unsubscribe, + name='unsubscribe'), ) if 'mellon' in settings.INSTALLED_APPS: diff --git a/corbo/views.py b/corbo/views.py index 272e0ee..27335bf 100644 --- a/corbo/views.py +++ b/corbo/views.py @@ -1,12 +1,14 @@ from datetime import datetime from django.conf import settings +from django.core import signing from django.core.urlresolvers import reverse from django.views.generic import CreateView, UpdateView, DeleteView, \ ListView, TemplateView from django.contrib.syndication.views import Feed from django.utils.feedgenerator import Atom1Feed from django.utils.http import urlencode +from django.http import Http404 import models from .forms import AnnounceForm, CategoryForm @@ -79,6 +81,29 @@ class CategoryDeleteView(DeleteView): delete_category = CategoryDeleteView.as_view() +class UnsubscribeView(DeleteView): + model = models.Subscription + + def get_object(self, queryset=None): + data = signing.loads(self.kwargs['unsubscription_token']) + try: + return models.Subscription.objects.get(category__pk=data['category'], + identifier=data['identifier']) + except models.Subscription.DoesNotExist: + raise Http404 + + def get_success_url(self): + return reverse('unsubscription_done') + +unsubscribe = UnsubscribeView.as_view() + + +class UnsubscriptionDoneView(TemplateView): + template_name='corbo/unsubscription_done.html' + +unsubscription_done = UnsubscriptionDoneView.as_view() + + class ManageView(ListView): paginate_by = settings.ANNOUNCES_PER_PAGE template_name = 'corbo/manage.html' diff --git a/tests/test_emailing.py b/tests/test_emailing.py index e7a134d..42ca111 100644 --- a/tests/test_emailing.py +++ b/tests/test_emailing.py @@ -5,9 +5,10 @@ import os from django.core.urlresolvers import reverse from django.utils.http import urlencode -from django.core import mail +from django.core import mail, signing from django.utils import timezone from django.core.files.storage import DefaultStorage +from django.core.urlresolvers import reverse from corbo.models import Category, Announce, Subscription, Broadcast from corbo.models import channel_choices @@ -91,3 +92,25 @@ def test_check_inline_images(app, categories, announces): assert storage.url(image_name) in mail.outbox[0].attachments.keys() mail.outbox = [] storage.delete(image_name) + +def test_unsubscription_link(app, categories, announces): + for category in categories: + uuid = uuid4() + email = '%s@example.net' % uuid + s = Subscription.objects.create(category=category, + identifier=email, + uuid=str(uuid)) + for announce in announces: + if announce.category != category: + continue + broadcast= Broadcast.objects.get(announce=announce) + broadcast.send() + assert broadcast.result + assert mail.outbox + signature = signing.dumps({'category': announce.category.pk, + 'identifier': email}) + unsubscription_link = reverse('unsubscribe', kwargs={'unsubscription_token': signature}) + assert mail.outbox[0].subject == announce.title + assert unsubscription_link in mail.outbox[0].html + assert unsubscription_link in mail.outbox[0].text + mail.outbox = [] -- 2.8.1