diff --git a/lms/djangoapps/notification_prefs/tests.py b/lms/djangoapps/notification_prefs/tests.py
index 06a7521016..f464527df3 100644
--- a/lms/djangoapps/notification_prefs/tests.py
+++ b/lms/djangoapps/notification_prefs/tests.py
@@ -9,13 +9,14 @@ from django.test.utils import override_settings
from mock import Mock, patch
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 user_api.models import UserPreference
+from util.testing import UrlResetMixin
@override_settings(SECRET_KEY="test secret key")
-class NotificationPrefViewTest(TestCase):
+class NotificationPrefViewTest(UrlResetMixin, TestCase):
INITIALIZATION_VECTOR = "\x00" * 16
@classmethod
@@ -23,7 +24,9 @@ class NotificationPrefViewTest(TestCase):
# Make sure global state is set up appropriately
Client().get("/")
+ @patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def setUp(self):
+ super(NotificationPrefViewTest, self).setUp()
self.user = UserFactory.create(username="testuser")
# Tokens are intentionally hard-coded instead of computed to help us
# avoid breaking existing links.
@@ -48,10 +51,12 @@ class NotificationPrefViewTest(TestCase):
def assertPrefValid(self, user):
"""Ensure that the correct preference for the user is persisted"""
- self.assertEqual(
- UserPreference.objects.get(user=user, key=NOTIFICATION_PREF_KEY).value,
- self.tokens[user]
- )
+ pref = UserPreference.objects.get(user=user, key=NOTIFICATION_PREF_KEY)
+ self.assertTrue(pref) # check exists and only 1 (.get)
+ # 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):
"""Ensure that the user does not have a persisted preference"""
@@ -174,13 +179,13 @@ class NotificationPrefViewTest(TestCase):
def test_unsubscribe_post(self):
request = self.request_factory.post("dummy")
- response = unsubscribe(request, "dummy")
+ response = set_subscription(request, "dummy", subscribe=False)
self.assertEqual(response.status_code, 405)
def test_unsubscribe_invalid_token(self):
def test_invalid_token(token, message):
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
test_invalid_token("ZOMG INVALID BASE64 CHARS!!!", "base64url")
@@ -215,7 +220,7 @@ class NotificationPrefViewTest(TestCase):
def test_user(user):
request = self.request_factory.get("dummy")
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.assertNotPrefExists(user)
@@ -226,7 +231,20 @@ class NotificationPrefViewTest(TestCase):
self.create_prefs()
request = self.request_factory.get("dummy")
request.user = AnonymousUser()
- unsubscribe(request, self.tokens[self.user])
- response = unsubscribe(request, self.tokens[self.user])
+ set_subscription(request, self.tokens[self.user], False)
+ response = set_subscription(request, self.tokens[self.user], subscribe=False)
self.assertEqual(response.status_code, 200)
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)
diff --git a/lms/djangoapps/notification_prefs/views.py b/lms/djangoapps/notification_prefs/views.py
index 68e8c875da..e1d64cb84f 100644
--- a/lms/djangoapps/notification_prefs/views.py
+++ b/lms/djangoapps/notification_prefs/views.py
@@ -153,12 +153,14 @@ def ajax_status(request):
@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
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
returned if the token parameter does not decrypt to a valid username. On
@@ -174,6 +176,13 @@ def unsubscribe(request, token):
except User.DoesNotExist:
raise Http404("username")
- UserPreference.objects.filter(user=user, key=NOTIFICATION_PREF_KEY).delete()
-
- return render_to_response("unsubscribe.html", {})
+ if subscribe:
+ UserPreference.objects.get_or_create(user=user,
+ 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})
diff --git a/lms/templates/resubscribe.html b/lms/templates/resubscribe.html
new file mode 100644
index 0000000000..e0bcfffe96
--- /dev/null
+++ b/lms/templates/resubscribe.html
@@ -0,0 +1,24 @@
+<%!
+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'/>
+
+
+
+
+
${_("Re-subscribe Successful!")}
+
+
+
+ ${_("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="".format(reverse('dashboard')),
+ link_end="",)}
+
- You will no longer receive notification emails from edX.
- Click here to return to your dashboard.
+ ${_("You will no longer receive forum notification emails from {platform_name}. "
+ "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="".format(reverse('dashboard')),
+ undo_link_start="".format(reverse('resubscribe_forum_update', args=[token])),
+ link_end="",)}
diff --git a/lms/urls.py b/lms/urls.py
index eabf4bf9b1..758d28aa92 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -338,7 +338,10 @@ if settings.COURSEWARE_ENABLED:
url(r'^notification_prefs/enable/', 'notification_prefs.views.ajax_enable'),
url(r'^notification_prefs/disable/', 'notification_prefs.views.ajax_disable'),
url(r'^notification_prefs/status/', 'notification_prefs.views.ajax_status'),
- url(r'^notification_prefs/unsubscribe/(?P[a-zA-Z0-9-_=]+)/', 'notification_prefs.views.unsubscribe'),
+ url(r'^notification_prefs/unsubscribe/(?P[a-zA-Z0-9-_=]+)/',
+ 'notification_prefs.views.set_subscription', {'subscribe': False}, name="unsubscribe_forum_update"),
+ url(r'^notification_prefs/resubscribe/(?P[a-zA-Z0-9-_=]+)/',
+ 'notification_prefs.views.set_subscription', {'subscribe': True}, name="resubscribe_forum_update"),
)
urlpatterns += (
# This MUST be the last view in the courseware--it's a catch-all for custom tabs.