Commit d9f98b84 by Jason Bau

Merge pull request #2226 from edx/jbau/resubscribe-notifer

resubscribe feature for notifier
parents 5bacfcc3 dfacbbdd
...@@ -9,13 +9,14 @@ from django.test.utils import override_settings ...@@ -9,13 +9,14 @@ from django.test.utils import override_settings
from mock import Mock, patch from mock import Mock, patch
from notification_prefs import NOTIFICATION_PREF_KEY from notification_prefs import NOTIFICATION_PREF_KEY
from notification_prefs.views import ajax_enable, ajax_disable, ajax_status, unsubscribe from notification_prefs.views import ajax_enable, ajax_disable, ajax_status, set_subscription, UsernameCipher
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from user_api.models import UserPreference from user_api.models import UserPreference
from util.testing import UrlResetMixin
@override_settings(SECRET_KEY="test secret key") @override_settings(SECRET_KEY="test secret key")
class NotificationPrefViewTest(TestCase): class NotificationPrefViewTest(UrlResetMixin, TestCase):
INITIALIZATION_VECTOR = "\x00" * 16 INITIALIZATION_VECTOR = "\x00" * 16
@classmethod @classmethod
...@@ -23,7 +24,9 @@ class NotificationPrefViewTest(TestCase): ...@@ -23,7 +24,9 @@ class NotificationPrefViewTest(TestCase):
# Make sure global state is set up appropriately # Make sure global state is set up appropriately
Client().get("/") Client().get("/")
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def setUp(self): def setUp(self):
super(NotificationPrefViewTest, self).setUp()
self.user = UserFactory.create(username="testuser") self.user = UserFactory.create(username="testuser")
# Tokens are intentionally hard-coded instead of computed to help us # Tokens are intentionally hard-coded instead of computed to help us
# avoid breaking existing links. # avoid breaking existing links.
...@@ -48,10 +51,12 @@ class NotificationPrefViewTest(TestCase): ...@@ -48,10 +51,12 @@ class NotificationPrefViewTest(TestCase):
def assertPrefValid(self, user): def assertPrefValid(self, user):
"""Ensure that the correct preference for the user is persisted""" """Ensure that the correct preference for the user is persisted"""
self.assertEqual( pref = UserPreference.objects.get(user=user, key=NOTIFICATION_PREF_KEY)
UserPreference.objects.get(user=user, key=NOTIFICATION_PREF_KEY).value, self.assertTrue(pref) # check exists and only 1 (.get)
self.tokens[user] # now coerce username to utf-8 encoded str, since we test with non-ascii unicdoe above and
) # the unittest framework has hard time coercing to unicode.
# decrypt also can't take a unicode input, so coerce its input to str
self.assertEqual(str(user.username.encode('utf-8')), UsernameCipher().decrypt(str(pref.value)))
def assertNotPrefExists(self, user): def assertNotPrefExists(self, user):
"""Ensure that the user does not have a persisted preference""" """Ensure that the user does not have a persisted preference"""
...@@ -174,13 +179,13 @@ class NotificationPrefViewTest(TestCase): ...@@ -174,13 +179,13 @@ class NotificationPrefViewTest(TestCase):
def test_unsubscribe_post(self): def test_unsubscribe_post(self):
request = self.request_factory.post("dummy") request = self.request_factory.post("dummy")
response = unsubscribe(request, "dummy") response = set_subscription(request, "dummy", subscribe=False)
self.assertEqual(response.status_code, 405) self.assertEqual(response.status_code, 405)
def test_unsubscribe_invalid_token(self): def test_unsubscribe_invalid_token(self):
def test_invalid_token(token, message): def test_invalid_token(token, message):
request = self.request_factory.get("dummy") request = self.request_factory.get("dummy")
self.assertRaisesRegexp(Http404, "^{}$".format(message), unsubscribe, request, token) self.assertRaisesRegexp(Http404, "^{}$".format(message), set_subscription, request, token, False)
# Invalid base64 encoding # Invalid base64 encoding
test_invalid_token("ZOMG INVALID BASE64 CHARS!!!", "base64url") test_invalid_token("ZOMG INVALID BASE64 CHARS!!!", "base64url")
...@@ -215,7 +220,7 @@ class NotificationPrefViewTest(TestCase): ...@@ -215,7 +220,7 @@ class NotificationPrefViewTest(TestCase):
def test_user(user): def test_user(user):
request = self.request_factory.get("dummy") request = self.request_factory.get("dummy")
request.user = AnonymousUser() request.user = AnonymousUser()
response = unsubscribe(request, self.tokens[user]) response = set_subscription(request, self.tokens[user], subscribe=False)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertNotPrefExists(user) self.assertNotPrefExists(user)
...@@ -226,7 +231,20 @@ class NotificationPrefViewTest(TestCase): ...@@ -226,7 +231,20 @@ class NotificationPrefViewTest(TestCase):
self.create_prefs() self.create_prefs()
request = self.request_factory.get("dummy") request = self.request_factory.get("dummy")
request.user = AnonymousUser() request.user = AnonymousUser()
unsubscribe(request, self.tokens[self.user]) set_subscription(request, self.tokens[self.user], False)
response = unsubscribe(request, self.tokens[self.user]) response = set_subscription(request, self.tokens[self.user], subscribe=False)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertNotPrefExists(self.user) self.assertNotPrefExists(self.user)
def test_resubscribe_success(self):
def test_user(user):
# start without a pref key
self.assertFalse(UserPreference.objects.filter(user=user, key=NOTIFICATION_PREF_KEY))
request = self.request_factory.get("dummy")
request.user = AnonymousUser()
response = set_subscription(request, self.tokens[user], subscribe=True)
self.assertEqual(response.status_code, 200)
self.assertPrefValid(user)
for user in self.tokens.keys():
test_user(user)
...@@ -153,12 +153,14 @@ def ajax_status(request): ...@@ -153,12 +153,14 @@ def ajax_status(request):
@require_GET @require_GET
def unsubscribe(request, token): def set_subscription(request, token, subscribe): # pylint: disable=unused-argument
""" """
A view that disables notifications for a user who may not be authenticated A view that disables or re-enables notifications for a user who may not be authenticated
This view is meant to be the target of an unsubscribe link. The request This view is meant to be the target of an unsubscribe link. The request
must be a GET, and the `token` parameter must decrypt to a valid username. must be a GET, and the `token` parameter must decrypt to a valid username.
The subscribe flag feature controls whether the view subscribes or unsubscribes the user, with subscribe=True
used to "undo" accidentally clicking on the unsubscribe link
A 405 will be returned if the request method is not GET. A 404 will be A 405 will be returned if the request method is not GET. A 404 will be
returned if the token parameter does not decrypt to a valid username. On returned if the token parameter does not decrypt to a valid username. On
...@@ -174,6 +176,13 @@ def unsubscribe(request, token): ...@@ -174,6 +176,13 @@ def unsubscribe(request, token):
except User.DoesNotExist: except User.DoesNotExist:
raise Http404("username") raise Http404("username")
UserPreference.objects.filter(user=user, key=NOTIFICATION_PREF_KEY).delete() if subscribe:
UserPreference.objects.get_or_create(user=user,
return render_to_response("unsubscribe.html", {}) key=NOTIFICATION_PREF_KEY,
defaults={
"value": UsernameCipher.encrypt(user.username)
})
return render_to_response("resubscribe.html", {'token': token})
else:
UserPreference.objects.filter(user=user, key=NOTIFICATION_PREF_KEY).delete()
return render_to_response("unsubscribe.html", {'token': token})
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.conf import settings
%>
<%inherit file="main.html" />
<%namespace name='static' file='static_content.html'/>
<section class="container unsubscribe">
<section class="message">
<h1>${_("Re-subscribe Successful!")}</h1>
<hr class="horizontal-divider">
<p>
${_("You have re-enabled forum notification emails from {platform_name}. "
"Click {dashboard_link_start}here{link_end} to return to your dashboard. ").format(
platform_name=settings.PLATFORM_NAME,
dashboard_link_start="<a href='{}'>".format(reverse('dashboard')),
link_end="</a>",)}
</p>
</section>
</section>
<%! from django.core.urlresolvers import reverse %> <%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.conf import settings
%>
<%inherit file="main.html" /> <%inherit file="main.html" />
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
...@@ -6,12 +10,17 @@ ...@@ -6,12 +10,17 @@
<section class="container unsubscribe"> <section class="container unsubscribe">
<section class="message"> <section class="message">
<h1>Unsubscribe Successful!</h1> <h1>${_("Unsubscribe Successful!")}</h1>
<hr class="horizontal-divider"> <hr class="horizontal-divider">
<p> <p>
You will no longer receive notification emails from edX. ${_("You will no longer receive forum notification emails from {platform_name}. "
Click <a href="${reverse('dashboard')}">here</a> to return to your dashboard. "Click {dashboard_link_start}here{link_end} to return to your dashboard. "
"If you did not mean to do this, click {undo_link_start}here{link_end} to re-subscribe.").format(
platform_name=settings.PLATFORM_NAME,
dashboard_link_start="<a href='{}'>".format(reverse('dashboard')),
undo_link_start="<a id='resub_link' href='{}'>".format(reverse('resubscribe_forum_update', args=[token])),
link_end="</a>",)}
</p> </p>
</section> </section>
</section> </section>
...@@ -338,7 +338,10 @@ if settings.COURSEWARE_ENABLED: ...@@ -338,7 +338,10 @@ if settings.COURSEWARE_ENABLED:
url(r'^notification_prefs/enable/', 'notification_prefs.views.ajax_enable'), url(r'^notification_prefs/enable/', 'notification_prefs.views.ajax_enable'),
url(r'^notification_prefs/disable/', 'notification_prefs.views.ajax_disable'), url(r'^notification_prefs/disable/', 'notification_prefs.views.ajax_disable'),
url(r'^notification_prefs/status/', 'notification_prefs.views.ajax_status'), url(r'^notification_prefs/status/', 'notification_prefs.views.ajax_status'),
url(r'^notification_prefs/unsubscribe/(?P<token>[a-zA-Z0-9-_=]+)/', 'notification_prefs.views.unsubscribe'), url(r'^notification_prefs/unsubscribe/(?P<token>[a-zA-Z0-9-_=]+)/',
'notification_prefs.views.set_subscription', {'subscribe': False}, name="unsubscribe_forum_update"),
url(r'^notification_prefs/resubscribe/(?P<token>[a-zA-Z0-9-_=]+)/',
'notification_prefs.views.set_subscription', {'subscribe': True}, name="resubscribe_forum_update"),
) )
urlpatterns += ( urlpatterns += (
# This MUST be the last view in the courseware--it's a catch-all for custom tabs. # This MUST be the last view in the courseware--it's a catch-all for custom tabs.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment