Skip to content

Commit cdcad43

Browse files
committed
Simplify community lists further by letting email subscriptions reuse
the existing infrastructure for accounts and emails, instead of a having a separate confirmation step - Legacy-Id: 10951
1 parent 5f4082d commit cdcad43

16 files changed

Lines changed: 157 additions & 225 deletions

File tree

ietf/community/forms.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -88,25 +88,24 @@ def restrict_state(state_type, slug=None):
8888
f.required = True
8989

9090

91-
class SubscriptionForm(forms.Form):
92-
notify_on = forms.ChoiceField(choices=[("all", "All changes"), ("significant", "Only significant state changes")], widget=forms.RadioSelect, initial="all")
93-
email = forms.EmailField(label="Your email")
94-
95-
def __init__(self, operation, clist, *args, **kwargs):
96-
self.operation = operation
91+
class SubscriptionForm(forms.ModelForm):
92+
def __init__(self, user, clist, *args, **kwargs):
9793
self.clist = clist
94+
self.user = user
9895

9996
super(SubscriptionForm, self).__init__(*args, **kwargs)
10097

101-
if operation == "subscribe":
102-
self.fields["notify_on"].label = "Get notified on"
103-
else:
104-
self.fields["notify_on"].label = "For notifications on"
98+
self.fields["notify_on"].widget = forms.RadioSelect(choices=self.fields["notify_on"].choices)
99+
self.fields["email"].queryset = self.fields["email"].queryset.filter(person__user=user, active=True).order_by("-primary")
100+
self.fields["email"].widget = forms.RadioSelect(choices=[t for t in self.fields["email"].choices if t[0]])
101+
102+
if self.fields["email"].queryset:
103+
self.fields["email"].initial = self.fields["email"].queryset[0]
105104

106105
def clean(self):
107-
if self.operation == "subscribe":
108-
if EmailSubscription.objects.filter(community_list=self.clist, email=self.cleaned_data["email"], significant=self.cleaned_data["notify_on"] == "significant").exists():
109-
raise forms.ValidationError("This email address is already subscribed.")
110-
else:
111-
if not EmailSubscription.objects.filter(community_list=self.clist, email=self.cleaned_data["email"], significant=self.cleaned_data["notify_on"] == "significant").exists():
112-
raise forms.ValidationError("Couldn't find a matching subscription?")
106+
if EmailSubscription.objects.filter(community_list=self.clist, email=self.cleaned_data["email"], notify_on=self.cleaned_data["notify_on"]).exists():
107+
raise forms.ValidationError("You already have a subscription like this.")
108+
109+
class Meta:
110+
model = EmailSubscription
111+
fields = ("notify_on", "email")

ietf/community/migrations/0003_cleanup.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,10 @@ class Migration(migrations.Migration):
105105
name='searchrule',
106106
unique_together=set([]),
107107
),
108+
migrations.AddField(
109+
model_name='emailsubscription',
110+
name='notify_on',
111+
field=models.CharField(default=b'all', max_length=30, choices=[(b'all', b'All changes'), (b'significant', b'Only significant state changes')]),
112+
preserve_default=True,
113+
),
108114
]

ietf/community/migrations/0004_cleanup_data.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import unicode_literals
33

4-
from django.db import migrations
4+
from django.db import migrations, models
55

66
def port_rules_to_typed_system(apps, schema_editor):
77
SearchRule = apps.get_model("community", "SearchRule")
@@ -163,6 +163,37 @@ def get_rid_of_empty_lists(apps, schema_editor):
163163
if not cl.added_docs.exists() and not cl.searchrule_set.exists() and not cl.emailsubscription_set.exists():
164164
cl.delete()
165165

166+
def move_email_subscriptions_to_preregistered_email(apps, schema_editor):
167+
EmailSubscription = apps.get_model("community", "EmailSubscription")
168+
Email = apps.get_model("person", "Email")
169+
Person = apps.get_model("person", "Person")
170+
171+
for e in EmailSubscription.objects.all():
172+
email_obj = None
173+
try:
174+
email_obj = Email.objects.get(address=e.email)
175+
except Email.DoesNotExist:
176+
if e.community_list.user:
177+
person = Person.objects.filter(user=e.community_list.user).first()
178+
179+
#print "creating", e.email, person.ascii
180+
# we'll register it on the user, on the assumption
181+
# that the user and the subscriber is the same person
182+
email_obj = Email.objects.create(
183+
address=e.email,
184+
person=person,
185+
)
186+
187+
if not email_obj:
188+
print "deleting", e.email
189+
e.delete()
190+
191+
def fill_in_notify_on(apps, schema_editor):
192+
EmailSubscription = apps.get_model("community", "EmailSubscription")
193+
194+
EmailSubscription.objects.filter(significant=False, notify_on="all")
195+
EmailSubscription.objects.filter(significant=True, notify_on="significant")
196+
166197
def noop(apps, schema_editor):
167198
pass
168199

@@ -175,9 +206,21 @@ class Migration(migrations.Migration):
175206
operations = [
176207
migrations.RunPython(port_rules_to_typed_system, delete_extra_person_rules),
177208
migrations.RunPython(rename_rule_type_forwards, rename_rule_type_backwards),
209+
migrations.RunPython(move_email_subscriptions_to_preregistered_email, noop),
178210
migrations.RunPython(get_rid_of_empty_lists, noop),
211+
migrations.RunPython(fill_in_notify_on, noop),
179212
migrations.RemoveField(
180213
model_name='searchrule',
181214
name='value',
182215
),
216+
migrations.AlterField(
217+
model_name='emailsubscription',
218+
name='email',
219+
field=models.ForeignKey(to='person.Email'),
220+
preserve_default=True,
221+
),
222+
migrations.RemoveField(
223+
model_name='emailsubscription',
224+
name='significant',
225+
),
183226
]

ietf/community/models.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from ietf.doc.models import Document, DocEvent, State
66
from ietf.group.models import Group
7-
from ietf.person.models import Person
7+
from ietf.person.models import Person, Email
88

99
class CommunityList(models.Model):
1010
user = models.ForeignKey(User, blank=True, null=True)
@@ -62,11 +62,16 @@ class SearchRule(models.Model):
6262

6363
class EmailSubscription(models.Model):
6464
community_list = models.ForeignKey(CommunityList)
65-
email = models.CharField(max_length=200)
66-
significant = models.BooleanField(default=False)
65+
email = models.ForeignKey(Email)
66+
67+
NOTIFICATION_CHOICES = [
68+
("all", "All changes"),
69+
("significant", "Only significant state changes")
70+
]
71+
notify_on = models.CharField(max_length=30, choices=NOTIFICATION_CHOICES, default="all")
6772

6873
def __unicode__(self):
69-
return u"%s to %s (%s changes)" % (self.email, self.community_list, "significant" if self.significant else "all")
74+
return u"%s to %s (%s changes)" % (self.email, self.community_list, self.notify_on)
7075

7176

7277
def notify_events(sender, instance, **kwargs):

ietf/community/resources.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ class Meta:
5151
#resource_name = 'emailsubscription'
5252
filtering = {
5353
"id": ALL,
54-
"email": ALL,
55-
"significant": ALL,
54+
"email": ALL_WITH_RELATIONS,
55+
"notify_on": ALL,
5656
"community_list": ALL_WITH_RELATIONS,
5757
}
5858
api.community.register(EmailSubscriptionResource())

ietf/community/tests.py

Lines changed: 15 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from ietf.community.utils import docs_matching_community_list_rule, community_list_rules_matching_doc
1010
from ietf.doc.models import State
1111
from ietf.doc.utils import add_state_change_event
12-
from ietf.person.models import Person
12+
from ietf.person.models import Person, Email
1313
from ietf.utils.test_data import make_test_data
1414
from ietf.utils.test_utils import login_testing_unauthorized, TestCase
1515
from ietf.utils.mail import outbox
@@ -217,28 +217,18 @@ def test_feed(self):
217217
self.assertEqual(r.status_code, 200)
218218
self.assertTrue('<entry>' not in r.content)
219219

220-
def extract_confirm_url(self, confirm_email):
221-
# dig out confirm_email link
222-
msg = confirm_email.get_payload(decode=True)
223-
line_start = "http"
224-
confirm_url = None
225-
for line in msg.split("\n"):
226-
if line.strip().startswith(line_start):
227-
confirm_url = line.strip()
228-
self.assertTrue(confirm_url)
229-
230-
return confirm_url
231-
232220
def test_subscription(self):
233221
draft = make_test_data()
234222

235-
url = urlreverse("community_personal_subscription", kwargs={ "operation": "subscribe", "username": "plain" })
223+
url = urlreverse("community_personal_subscription", kwargs={ "username": "plain" })
236224

237-
# subscribe without list
225+
login_testing_unauthorized(self, "plain", url)
226+
227+
# subscription without list
238228
r = self.client.get(url)
239229
self.assertEqual(r.status_code, 404)
240230

241-
# subscribe with list
231+
# subscription with list
242232
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
243233
clist.added_docs.add(draft)
244234
SearchRule.objects.create(
@@ -250,42 +240,19 @@ def test_subscription(self):
250240
r = self.client.get(url)
251241
self.assertEqual(r.status_code, 200)
252242

253-
# do subscribe
254-
mailbox_before = len(outbox)
255-
r = self.client.post(url, { "email": "subscriber@example.com", "notify_on": "significant" })
256-
self.assertEqual(r.status_code, 200)
257-
self.assertEqual(len(outbox), mailbox_before + 1)
258-
259-
# go to confirm page
260-
confirm_url = self.extract_confirm_url(outbox[-1])
261-
r = self.client.get(confirm_url)
262-
self.assertEqual(r.status_code, 200)
263-
264-
# confirm subscribe
265-
r = self.client.post(confirm_url, { 'action': 'confirm' })
243+
# subscribe
244+
email = Email.objects.filter(person__user__username="plain").first()
245+
r = self.client.post(url, { "email": email.pk, "notify_on": "significant", "action": "subscribe" })
266246
self.assertEqual(r.status_code, 302)
267-
self.assertEqual(EmailSubscription.objects.filter(community_list=clist, email="subscriber@example.com", significant=True).count(), 1)
268-
269-
# unsubscribe
270-
url = urlreverse("community_personal_subscription", kwargs={ "operation": "unsubscribe", "username": "plain" })
271-
r = self.client.get(url)
272-
self.assertEqual(r.status_code, 200)
273247

274-
# do unsubscribe
275-
mailbox_before = len(outbox)
276-
r = self.client.post(url, { "email": "subscriber@example.com", "notify_on": "significant" })
277-
self.assertEqual(r.status_code, 200)
278-
self.assertEqual(len(outbox), mailbox_before + 1)
248+
subscription = EmailSubscription.objects.filter(community_list=clist, email=email, notify_on="significant").first()
279249

280-
# go to confirm page
281-
confirm_url = self.extract_confirm_url(outbox[-1])
282-
r = self.client.get(confirm_url)
283-
self.assertEqual(r.status_code, 200)
250+
self.assertTrue(subscription)
284251

285-
# confirm unsubscribe
286-
r = self.client.post(confirm_url, { 'action': 'confirm' })
252+
# delete subscription
253+
r = self.client.post(url, { "subscription_id": subscription.pk, "action": "unsubscribe" })
287254
self.assertEqual(r.status_code, 302)
288-
self.assertEqual(EmailSubscription.objects.filter(community_list=clist, email="subscriber@example.com", significant=True).count(), 0)
255+
self.assertEqual(EmailSubscription.objects.filter(community_list=clist, email=email, notify_on="significant").count(), 0)
289256

290257
def test_notification(self):
291258
draft = make_test_data()
@@ -299,7 +266,7 @@ def test_notification(self):
299266
text="test",
300267
)
301268

302-
EmailSubscription.objects.create(community_list=clist, email="subscriber@example.com", significant=True)
269+
EmailSubscription.objects.create(community_list=clist, email=Email.objects.filter(person__user__username="plain").first(), notify_on="significant")
303270

304271
mailbox_before = len(outbox)
305272
active_state = State.objects.get(type="draft", slug="active")

ietf/community/urls.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,13 @@
88
url(r'^personal/(?P<username>[^/]+)/untrackdocument/(?P<name>[^/]+)/$', 'untrack_document', name='community_personal_untrack_document'),
99
url(r'^personal/(?P<username>[^/]+)/csv/$', 'export_to_csv', name='community_personal_csv'),
1010
url(r'^personal/(?P<username>[^/]+)/feed/$', 'feed', name='community_personal_feed'),
11-
url(r'^personal/(?P<username>[^/]+)/(?P<operation>subscribe|unsubscribe)/$', 'subscription', name='community_personal_subscription'),
12-
url(r'^personal/(?P<username>[^/]+)/(?P<operation>subscribe|unsubscribe)/confirm/(?P<auth>[^/]+)/$', 'confirm_subscription', name='community_personal_confirm_subscription'),
11+
url(r'^personal/(?P<username>[^/]+)/subscription/$', 'subscription', name='community_personal_subscription'),
1312

1413
url(r'^group/(?P<acronym>[\w.@+-]+)/$', 'view_list', name='community_group_view_list'),
1514
url(r'^group/(?P<acronym>[\w.@+-]+)/manage/$', 'manage_list', name='community_group_manage_list'),
1615
url(r'^group/(?P<acronym>[\w.@+-]+)/trackdocument/(?P<name>[^/]+)/$', 'track_document', name='community_group_track_document'),
1716
url(r'^group/(?P<acronym>[\w.@+-]+)/untrackdocument/(?P<name>[^/]+)/$', 'untrack_document', name='community_group_untrack_document'),
1817
url(r'^group/(?P<acronym>[\w.@+-]+)/csv/$', 'export_to_csv', name='community_group_csv'),
1918
url(r'^group/(?P<acronym>[\w.@+-]+)/feed/$', 'feed', name='community_group_feed'),
20-
url(r'^group/(?P<acronym>[^/]+)/(?P<operation>subscribe|unsubscribe)/$', 'subscription', name='community_group_subscription'),
21-
url(r'^group/(?P<acronym>[^/]+)/(?P<operation>subscribe|unsubscribe)/confirm/(?P<auth>[^/]+)/$', 'confirm_subscription', name='community_group_confirm_subscription'),
19+
url(r'^group/(?P<acronym>[\w.@+-]+)/subscription/$', 'subscription', name='community_group_subscription'),
2220
)

ietf/community/utils.py

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
from django.db.models import Q
22
from django.conf import settings
3-
from django.contrib.sites.models import Site
4-
from django.http import Http404
5-
import django.core.signing
63

74
from ietf.community.models import CommunityList, EmailSubscription, SearchRule
85
from ietf.doc.models import Document, State
@@ -143,49 +140,14 @@ def notify_event_to_subscribers(event):
143140
subscriptions = EmailSubscription.objects.filter(community_list__in=community_lists_tracking_doc(event.doc)).distinct()
144141

145142
if not significant:
146-
subscriptions = subscriptions.filter(significant=False)
143+
subscriptions = subscriptions.filter(notify_on="all")
147144

148-
for sub in subscriptions.select_related("community_list"):
145+
for sub in subscriptions.select_related("community_list", "email"):
149146
clist = sub.community_list
150147
subject = '%s notification: Changes to %s' % (clist.long_name(), event.doc.name)
151148

152-
send_mail(None, sub.email, settings.DEFAULT_FROM_EMAIL, subject, 'community/notification_email.txt',
149+
send_mail(None, sub.email.address, settings.DEFAULT_FROM_EMAIL, subject, 'community/notification_email.txt',
153150
context = {
154151
'event': event,
155152
'clist': clist,
156153
})
157-
158-
def confirmation_salt(operation, clist):
159-
return ":".join(["community",
160-
operation,
161-
"personal" if clist.user else "group",
162-
clist.user.username if clist.user else clist.group.acronym])
163-
164-
def send_subscription_confirmation_email(request, clist, operation, to_email, significant):
165-
domain = Site.objects.get_current().domain
166-
subject = 'Confirm list subscription: %s' % clist
167-
from_email = settings.DEFAULT_FROM_EMAIL
168-
169-
auth = django.core.signing.dumps([to_email, 1 if significant else 0], salt=confirmation_salt("subscribe", clist))
170-
171-
send_mail(request, to_email, from_email, subject, 'community/confirm_email.txt', {
172-
'domain': domain,
173-
'clist': clist,
174-
'auth': auth,
175-
'operation': operation,
176-
})
177-
178-
def verify_confirmation_data(auth, clist, operation):
179-
try:
180-
data = django.core.signing.loads(auth, salt=confirmation_salt(operation, clist), max_age=24 * 60 * 60)
181-
except django.core.signing.BadSignature:
182-
raise Http404("Invalid or expired auth")
183-
184-
try:
185-
to_email, significant = data[:2]
186-
except ValueError:
187-
raise Http404("Invalid data")
188-
189-
return to_email, bool(significant)
190-
191-

0 commit comments

Comments
 (0)