0001-unsubscribe-link-10795.patch
corbo/models.py | ||
---|---|---|
9 | 9 |
from django.db import models |
10 | 10 |
from django.core.files.storage import DefaultStorage |
11 | 11 |
from django.utils.translation import ugettext_lazy as _ |
12 |
from django.core import signing |
|
13 |
from django.template import loader, Context |
|
14 |
from django.core.urlresolvers import reverse |
|
12 | 15 | |
13 | 16 |
from ckeditor.fields import RichTextField |
14 | 17 | |
... | ... | |
75 | 78 |
subscriptions = self.announce.category.subscription_set.all() |
76 | 79 |
total_sent = 0 |
77 | 80 |
handler = HTML2Text() |
78 |
m = Message(html=self.announce.text, subject=self.announce.title, |
|
79 |
text=handler.handle(self.announce.text), |
|
80 |
mail_from=settings.DEFAULT_FROM_EMAIL) |
|
81 |
template = loader.get_template('corbo/announce.html') |
|
82 |
m = Message(subject=self.announce.title, mail_from=settings.DEFAULT_FROM_EMAIL) |
|
81 | 83 |
html_tree = HTMLTree(self.announce.text) |
82 | 84 |
storage = DefaultStorage() |
83 | 85 |
for img in html_tree.xpath('//img/@src'): |
84 | 86 |
img_path = img.lstrip(settings.MEDIA_URL) |
85 | 87 |
m.attach(filename=img, data=storage.open(img_path)) |
86 | 88 |
m.attachments[img].is_inline = True |
87 |
m.transformer.synchronize_inline_images() |
|
88 |
m.transformer.save() |
|
89 | 89 |
for s in subscriptions: |
90 | 90 |
if not s.identifier: |
91 | 91 |
continue |
92 |
unsubscribe_token = signing.dumps({'category': self.announce.category.pk, |
|
93 |
'identifier': s.identifier}) |
|
94 |
unsubscribe_link = reverse('unsubscribe', kwargs={'unsubscription_token': unsubscribe_token}) |
|
95 |
message = template.render(Context({'unsubscribe_link': unsubscribe_link, |
|
96 |
'content': self.announce.text})) |
|
97 |
m.html = message |
|
98 |
m.transformer.synchronize_inline_images() |
|
99 |
m.transformer.save() |
|
100 |
m.text = handler.handle(message) |
|
101 | ||
92 | 102 |
sent = m.send(to=s.identifier) |
93 | 103 |
if sent: |
94 | 104 |
total_sent += 1 |
corbo/static/css/corbo.css | ||
---|---|---|
280 | 280 | |
281 | 281 |
#id_transport_channel li, #id_transport_channel label { |
282 | 282 |
display: inline; |
283 |
} |
|
284 | ||
285 |
.content { |
|
286 |
width: 50%; |
|
287 |
margin: auto; |
|
288 |
} |
|
289 | ||
290 |
.unsubscription { |
|
291 |
background: #e6db74; |
|
292 |
border: 1px solid #aaa; |
|
293 |
padding: 5px; |
|
294 |
} |
|
295 | ||
296 |
.info { |
|
297 |
background: #a6e22e; |
|
298 |
border: 1px solid #aaa; |
|
299 |
padding: 5px; |
|
283 | 300 |
} |
corbo/templates/corbo/announce.html | ||
---|---|---|
1 |
{% load i18n %} |
|
2 |
<div class="content"> |
|
3 |
{{ content|safe }} |
|
4 |
</div> |
|
5 |
<div class="footer"> |
|
6 |
{% blocktrans %} |
|
7 |
Click <a href='{{ unsubscribe_link}}'>here</a> to unsubscribe from this newsletter. |
|
8 |
{% endblocktrans %} |
|
9 |
</div> |
corbo/templates/corbo/subscription_confirm_delete.html | ||
---|---|---|
1 |
{% extends "corbo/unsubscription.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block title %}{% blocktrans with object.category as category %} |
|
5 |
Unsubscription from {{ category }} |
|
6 |
{% endblocktrans %}{% endblock %} |
|
7 | ||
8 |
{% block content %} |
|
9 |
<div class="unsubscription"> |
|
10 |
{% blocktrans with object.category as category and object.identifier as identifier %} |
|
11 |
Are you sure you want to unsubscribe {{ identifier }} from "{{ category }}"? |
|
12 |
{% endblocktrans %} |
|
13 |
<form method='POST'> |
|
14 |
{% csrf_token %} |
|
15 |
<button>{% trans "Unsubscribe" %}</button> |
|
16 |
</form> |
|
17 |
</div> |
|
18 |
{% endblock %} |
corbo/templates/corbo/unsubscription.html | ||
---|---|---|
1 |
{% load static %} |
|
2 |
<!DOCTYPE html> |
|
3 |
<html> |
|
4 |
<head> |
|
5 |
<title>{% block title %}{% endblock %}</title> |
|
6 |
<link rel='stylesheet' type='text/css' href='{% static "css/corbo.css" %}' /> |
|
7 |
</head> |
|
8 |
<body> |
|
9 |
<div class='content'> |
|
10 |
{% block content %} |
|
11 |
{% endblock %} |
|
12 |
</div> |
|
13 |
</body> |
|
14 |
</html> |
corbo/templates/corbo/unsubscription_done.html | ||
---|---|---|
1 |
{% extends "corbo/unsubscription.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block title %} |
|
5 |
{% trans "Successfully unsubscription" %} |
|
6 |
{% endblock %} |
|
7 | ||
8 |
{% block content %} |
|
9 |
<div class="info">{% trans "You were sucessfully unsubcribed." %}</div> |
|
10 |
{% endblock %} |
corbo/urls.py | ||
---|---|---|
5 | 5 |
from django.contrib import admin |
6 | 6 | |
7 | 7 |
from .urls_utils import decorated_includes, manager_required |
8 |
from .views import homepage, atom |
|
8 |
from .views import homepage, atom, unsubscribe, unsubscription_done
|
|
9 | 9 | |
10 | 10 |
from manage_urls import urlpatterns as manage_urls |
11 | 11 |
from api_urls import urlpatterns as api_urls |
... | ... | |
17 | 17 |
include(manage_urls))), |
18 | 18 |
url(r'^ckeditor/', include('ckeditor.urls')), |
19 | 19 |
url(r'^admin/', include(admin.site.urls)), |
20 |
url(r'^api/', include(api_urls)) |
|
20 |
url(r'^api/', include(api_urls)), |
|
21 |
url(r'^unsubscribe/done/$', unsubscription_done, |
|
22 |
name='unsubscription_done'), |
|
23 |
url(r'^unsubscribe/(?P<unsubscription_token>[\w:-]+)$', unsubscribe, |
|
24 |
name='unsubscribe'), |
|
21 | 25 |
) |
22 | 26 | |
23 | 27 |
if 'mellon' in settings.INSTALLED_APPS: |
corbo/views.py | ||
---|---|---|
1 | 1 |
from datetime import datetime |
2 | 2 | |
3 | 3 |
from django.conf import settings |
4 |
from django.core import signing |
|
4 | 5 |
from django.core.urlresolvers import reverse |
5 | 6 |
from django.views.generic import CreateView, UpdateView, DeleteView, \ |
6 | 7 |
ListView, TemplateView |
7 | 8 |
from django.contrib.syndication.views import Feed |
8 | 9 |
from django.utils.feedgenerator import Atom1Feed |
9 | 10 |
from django.utils.http import urlencode |
11 |
from django.http import Http404 |
|
10 | 12 | |
11 | 13 |
import models |
12 | 14 |
from .forms import AnnounceForm, CategoryForm |
... | ... | |
79 | 81 |
delete_category = CategoryDeleteView.as_view() |
80 | 82 | |
81 | 83 | |
84 |
class UnsubscribeView(DeleteView): |
|
85 |
model = models.Subscription |
|
86 | ||
87 |
def get_object(self, queryset=None): |
|
88 |
data = signing.loads(self.kwargs['unsubscription_token']) |
|
89 |
try: |
|
90 |
return models.Subscription.objects.get(category__pk=data['category'], |
|
91 |
identifier=data['identifier']) |
|
92 |
except models.Subscription.DoesNotExist: |
|
93 |
raise Http404 |
|
94 | ||
95 |
def get_success_url(self): |
|
96 |
return reverse('unsubscription_done') |
|
97 | ||
98 |
unsubscribe = UnsubscribeView.as_view() |
|
99 | ||
100 | ||
101 |
class UnsubscriptionDoneView(TemplateView): |
|
102 |
template_name='corbo/unsubscription_done.html' |
|
103 | ||
104 |
unsubscription_done = UnsubscriptionDoneView.as_view() |
|
105 | ||
106 | ||
82 | 107 |
class ManageView(ListView): |
83 | 108 |
paginate_by = settings.ANNOUNCES_PER_PAGE |
84 | 109 |
template_name = 'corbo/manage.html' |
tests/test_emailing.py | ||
---|---|---|
2 | 2 |
import json |
3 | 3 |
from uuid import uuid4 |
4 | 4 | |
5 | ||
6 | 5 |
from django.core.urlresolvers import reverse |
7 | 6 |
from django.utils.http import urlencode |
8 |
from django.core import mail |
|
7 |
from django.core import mail, signing
|
|
9 | 8 |
from django.utils import timezone |
9 |
from django.core.urlresolvers import reverse |
|
10 | 10 | |
11 | 11 |
from corbo.models import Category, Announce, Subscription, Broadcast |
12 | 12 |
from corbo.models import channel_choices |
... | ... | |
57 | 57 |
broadcast.send() |
58 | 58 |
assert broadcast.result |
59 | 59 |
assert mail.outbox |
60 | ||
61 |
def test_unsubscription_link(app, categories, announces): |
|
62 |
for category in categories: |
|
63 |
uuid = uuid4() |
|
64 |
email = '%s@example.net' % uuid |
|
65 |
s = Subscription.objects.create(category=category, |
|
66 |
identifier=email, |
|
67 |
uuid=str(uuid)) |
|
68 |
for announce in announces: |
|
69 |
if announce.category != category: |
|
70 |
continue |
|
71 |
broadcast= Broadcast.objects.get(announce=announce) |
|
72 |
broadcast.send() |
|
73 |
assert broadcast.result |
|
74 |
assert mail.outbox |
|
75 |
for out in mail.outbox: |
|
76 |
signature = signing.dumps({'category': announce.category.pk, |
|
77 |
'identifier': email}) |
|
78 |
unsubscription_link = reverse('unsubscribe', kwargs={'unsubscription_token': signature}) |
|
79 |
assert out.subject == announce.title |
|
80 |
assert unsubscription_link in out.html |
|
81 |
assert unsubscription_link in out.text |
|
82 |
mail.outbox = [] |
|
60 |
- |