Commit fa298387 by Waheed Ahmed Committed by GitHub

Merge pull request #378 from edx/waheed/ecom-5408-add-email-notification-switch

Added email notification ON/OFF switch.
parents ea5bf41b 6dc823c5
......@@ -15,6 +15,7 @@
"moment": "~2.13.0",
"pikaday": "https://github.com/owenmead/Pikaday.git#1.4.0",
"clipboard": "1.5.12",
"jquery-ui": "1.10.3"
"jquery-ui": "1.10.3",
"js-cookie": "2.1.2"
}
}
""" Publisher context processors. """
from course_discovery.apps.publisher.utils import is_email_notification_enabled
def publisher(request):
return {
'is_email_notification_enabled': is_email_notification_enabled(request.user)
}
""" Publisher context processor tests. """
from django.test import TestCase, RequestFactory
from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.publisher.context_processors import publisher
from course_discovery.apps.publisher.utils import is_email_notification_enabled
class PublisherContextProcessorTests(TestCase):
""" Tests for publisher.context_processors.publisher """
def test_publisher(self):
request = RequestFactory().get('/')
request.user = UserFactory()
self.assertDictEqual(
publisher(request), {'is_email_notification_enabled': is_email_notification_enabled(request.user)}
)
""" Tests publisher.utils"""
from django.test import TestCase
from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.publisher.tests import factories
from course_discovery.apps.publisher.utils import is_email_notification_enabled
class PublisherUtilsTests(TestCase):
""" Tests for the publisher utils. """
def setUp(self):
super(PublisherUtilsTests, self).setUp()
self.user = UserFactory(is_staff=True, is_superuser=True)
def test_email_notification_enabled_by_default(self):
""" Test email notification is enabled for the user by default."""
self.assertFalse(hasattr(self.user, 'attributes'))
# Verify email notifications are enabled for user without associated attributes
self.assertEqual(is_email_notification_enabled(self.user), True)
def test_is_email_notification_enabled(self):
""" Test email notification enabled/disabled for the user."""
user_attribute = factories.UserAttributeFactory(user=self.user)
# Verify email notifications are enabled for user with associated attributes,
# but no explicit value set for the enable_email_notification attribute
self.assertEqual(is_email_notification_enabled(self.user), True)
# Disabled email notification
user_attribute.enable_email_notification = False
user_attribute.save() # pylint: disable=no-member
# Verify that email notifications are disabled for the user
self.assertEqual(is_email_notification_enabled(self.user), False)
# pylint: disable=no-member
import json
import ddt
from mock import patch
......@@ -10,11 +12,13 @@ from django.forms import model_to_dict
from django.test import TestCase
from guardian.shortcuts import assign_perm
from course_discovery.apps.core.models import User
from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD
from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.publisher.models import Course, CourseRun, Seat, State
from course_discovery.apps.publisher.tests import factories
from course_discovery.apps.publisher.tests.utils import create_non_staff_user_and_login
from course_discovery.apps.publisher.utils import is_email_notification_enabled
from course_discovery.apps.publisher.views import CourseRunDetailView
from course_discovery.apps.publisher.wrappers import CourseRunWrapper
from course_discovery.apps.publisher_comments.tests.factories import CommentFactory
......@@ -942,3 +946,36 @@ class CourseRunListViewTests(TestCase):
# Verify that we have 2 in progress and 1 published course runs
self.assertEqual(len(response.context['object_list']), 2)
self.assertEqual(len(response.context['published_courseruns']), 1)
class ToggleEmailNotificationTests(TestCase):
""" Tests for `ToggleEmailNotification` view. """
def setUp(self):
super(ToggleEmailNotificationTests, self).setUp()
self.user = UserFactory()
self.client.login(username=self.user.username, password=USER_PASSWORD)
self.toggle_email_settings_url = reverse('publisher:publisher_toggle_email_settings')
def test_toggle_email_notification(self):
""" Test that user can toggle email notification settings."""
# Verify that by default email notification is enabled for the user
self.assertEqual(is_email_notification_enabled(self.user), True)
# Verify that user can disable email notifications
self.assert_toggle_email_notification(False)
# Verify that user can enable email notifications
self.assert_toggle_email_notification(True)
def assert_toggle_email_notification(self, is_enabled):
""" Assert user can toggle email notifications."""
response = self.client.post(self.toggle_email_settings_url, data={'is_enabled': json.dumps(is_enabled)})
self.assertEqual(response.status_code, 200)
# Reload user object from database to test the changes
user = User.objects.get(username=self.user.username)
self.assertEqual(is_email_notification_enabled(user), is_enabled)
......@@ -21,4 +21,8 @@ urlpatterns = [
),
url(r'^seats/new$', views.CreateSeatView.as_view(), name='publisher_seats_new'),
url(r'^seats/(?P<pk>\d+)/edit/$', views.UpdateSeatView.as_view(), name='publisher_seats_edit'),
url(
r'^user/toggle/email_settings/$',
views.ToggleEmailNotification.as_view(),
name='publisher_toggle_email_settings'),
]
""" Publisher Utils."""
def is_email_notification_enabled(user):
""" Check email notification is enabled for the user.
Arguments:
user (User): User object
Returns:
enable_email_notification | True
"""
if hasattr(user, 'attributes'):
return user.attributes.enable_email_notification
return True
"""
Course publisher views.
"""
import json
from django.contrib import messages
from django.contrib.auth.models import Group
from django.core.urlresolvers import reverse
from django.db import transaction
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http import HttpResponseRedirect, HttpResponseForbidden, JsonResponse
from django.shortcuts import render, get_object_or_404
from django.utils.translation import ugettext_lazy as _
from django.views.generic import View
......@@ -19,7 +21,7 @@ from course_discovery.apps.publisher.forms import (
CourseForm, CourseRunForm, SeatForm, CustomCourseForm, CustomCourseRunForm, CustomSeatForm
)
from course_discovery.apps.publisher import mixins
from course_discovery.apps.publisher.models import Course, CourseRun, Seat, State
from course_discovery.apps.publisher.models import Course, CourseRun, Seat, State, UserAttributes
from course_discovery.apps.publisher.wrappers import CourseRunWrapper
......@@ -246,3 +248,15 @@ class ChangeStateView(mixins.LoginRequiredMixin, View):
except (CourseRun.DoesNotExist, TransitionNotAllowed):
messages.error(request, _('There was an error in changing state.'))
return HttpResponseRedirect(reverse('publisher:publisher_course_run_detail', kwargs={'pk': course_run_id}))
class ToggleEmailNotification(mixins.LoginRequiredMixin, View):
""" Toggle User Email Notification Settings."""
def post(self, request):
is_enabled = json.loads(request.POST.get('is_enabled'))
user_attribute, __ = UserAttributes.objects.get_or_create(user=request.user)
user_attribute.enable_email_notification = is_enabled
user_attribute.save()
return JsonResponse({'is_enabled': is_enabled})
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-10-19 18:33+0000\n"
"POT-Creation-Date: 2016-10-21 07:12+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -258,7 +258,7 @@ msgstr ""
#: course_discovery/apps/course_metadata/models.py:186
#: course_discovery/apps/course_metadata/models.py:234
#: course_discovery/apps/course_metadata/models.py:325
#: course_discovery/apps/course_metadata/models.py:578
#: course_discovery/apps/course_metadata/models.py:584
msgid "UUID"
msgstr ""
......@@ -308,55 +308,55 @@ msgstr ""
msgid "Estimated number of weeks needed to complete this course run."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:465
#: course_discovery/apps/course_metadata/models.py:471
msgid "Archived"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:467
#: course_discovery/apps/course_metadata/models.py:473
msgid "Current"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:469
#: course_discovery/apps/course_metadata/models.py:475
msgid "Starting Soon"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:471
#: course_discovery/apps/course_metadata/models.py:477
msgid "Upcoming"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:507
#: course_discovery/apps/course_metadata/models.py:513
#: course_discovery/apps/publisher/models.py:280
msgid "Honor"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:508
#: course_discovery/apps/course_metadata/models.py:514
#: course_discovery/apps/publisher/models.py:281
msgid "Audit"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:509
#: course_discovery/apps/course_metadata/models.py:515
#: course_discovery/apps/publisher/models.py:282
msgid "Verified"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:510
#: course_discovery/apps/course_metadata/models.py:516
msgid "Professional"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:511
#: course_discovery/apps/course_metadata/models.py:517
#: course_discovery/apps/publisher/models.py:285
msgid "Credit"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:558
#: course_discovery/apps/course_metadata/models.py:564
msgid "FAQ"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:559
#: course_discovery/apps/course_metadata/models.py:565
msgid "FAQs"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:568
#: course_discovery/apps/course_metadata/models.py:574
msgid ""
"Seat types that qualify for completion of programs of this type. Learners "
"completing associated courses, but enrolled in other seat types, will NOT "
......@@ -364,43 +364,43 @@ msgid ""
"program."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:580
#: course_discovery/apps/course_metadata/models.py:586
msgid "The user-facing display title for this Program."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:582
#: course_discovery/apps/course_metadata/models.py:588
msgid "A brief, descriptive subtitle for the Program."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:585
#: course_discovery/apps/course_metadata/models.py:591
msgid "The lifecycle status of this Program."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:589
#: course_discovery/apps/course_metadata/models.py:595
msgid "Slug used to generate links to the marketing site"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:593
#: course_discovery/apps/course_metadata/models.py:599
msgid ""
"If this box is not checked, courses will be ordered as in the courses select "
"box above."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:603
#: course_discovery/apps/course_metadata/models.py:609
msgid ""
"This field is now deprecated (ECOM-6021).Estimated number of weeks needed to "
"complete a course run belonging to this program."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:619
#: course_discovery/apps/course_metadata/models.py:625
msgid "Image used atop detail pages"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:620
#: course_discovery/apps/course_metadata/models.py:626
msgid "Image used for discovery cards"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:632
#: course_discovery/apps/course_metadata/models.py:638
msgid "The description of credit redemption for courses in program"
msgstr ""
......@@ -540,20 +540,20 @@ msgstr ""
msgid "Professional (no ID verifiation)"
msgstr ""
#: course_discovery/apps/publisher/views.py:118
#: course_discovery/apps/publisher/views.py:120
msgid "Course created successfully."
msgstr ""
#: course_discovery/apps/publisher/views.py:124
#: course_discovery/apps/publisher/views.py:126
msgid "Please fill all required field."
msgstr ""
#: course_discovery/apps/publisher/views.py:243
#: course_discovery/apps/publisher/views.py:245
#, python-brace-format
msgid "Content moved to `{state}` successfully."
msgstr ""
#: course_discovery/apps/publisher/views.py:247
#: course_discovery/apps/publisher/views.py:249
msgid "There was an error in changing state."
msgstr ""
......@@ -577,11 +577,11 @@ msgstr ""
msgid "modified"
msgstr ""
#: course_discovery/templates/base.html:49
#: course_discovery/templates/base.html:52
msgid "Course Runs"
msgstr ""
#: course_discovery/templates/base.html:54
#: course_discovery/templates/base.html:57
msgid "Courses"
msgstr ""
......@@ -662,7 +662,15 @@ msgstr ""
msgid "Email notifications"
msgstr ""
#: course_discovery/templates/header.html:43
#: course_discovery/templates/header.html:45
msgid "ON"
msgstr ""
#: course_discovery/templates/header.html:45
msgid "OFF"
msgstr ""
#: course_discovery/templates/header.html:48
msgid "Sign Out"
msgstr ""
......
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-10-19 18:34+0000\n"
"POT-Creation-Date: 2016-10-21 07:12+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -20,3 +20,11 @@ msgstr ""
#: course_discovery/static/js/catalogs-change-form.js:17
msgid "Preview"
msgstr ""
#: course_discovery/static/js/publisher/views/navbar.js:33
msgid "OFF"
msgstr ""
#: course_discovery/static/js/publisher/views/navbar.js:46
msgid "ON"
msgstr ""
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-10-19 18:33+0000\n"
"POT-Creation-Date: 2016-10-21 07:12+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -785,6 +785,14 @@ msgid "Email notifications"
msgstr "Émäïl nötïfïçätïöns Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#"
#: course_discovery/templates/header.html
msgid "ON"
msgstr "ÖN Ⱡ'σя#"
#: course_discovery/templates/header.html
msgid "OFF"
msgstr "ÖFF Ⱡ'σяєм#"
#: course_discovery/templates/header.html
msgid "Sign Out"
msgstr "Sïgn Öüt Ⱡ'σяєм ιρѕυм ∂#"
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-10-19 18:34+0000\n"
"POT-Creation-Date: 2016-10-21 07:12+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -20,3 +20,11 @@ msgstr ""
#: course_discovery/static/js/catalogs-change-form.js
msgid "Preview"
msgstr "Prévïéw Ⱡ'σяєм ιρѕυм #"
#: course_discovery/static/js/publisher/views/navbar.js
msgid "OFF"
msgstr "ÖFF Ⱡ'σяєм#"
#: course_discovery/static/js/publisher/views/navbar.js
msgid "ON"
msgstr "ÖN Ⱡ'σя#"
......@@ -178,6 +178,7 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.messages.context_processors.messages',
'course_discovery.apps.core.context_processors.core',
'course_discovery.apps.publisher.context_processors.publisher',
),
'debug': True, # Django will only display debug pages if the global DEBUG setting is set to True.
}
......
$(document).ready(function () {
$("body").click(function() {
$('.nav-account .wrapper-nav-sub').removeClass('is-shown');
$('.nav-account .nav-account-user .title').removeClass('is-selected');
});
$('.nav-account .nav-item, .wrapper-nav-sub').click(function(e) {
$subnav = $('.nav-account').find('.wrapper-nav-sub');
$title = $(this).find('.title');
if ($subnav.hasClass('is-shown')) {
$subnav.removeClass('is-shown');
$title.removeClass('is-selected');
} else {
$('.nav-account .nav-item .title').removeClass('is-selected');
$('.nav-account .wrapper-nav-sub').removeClass('is-shown');
$title.addClass('is-selected');
$subnav.addClass('is-shown');
// if propagation is not stopped, the event will bubble up to the
// body element, which will close the dropdown.
e.stopPropagation();
}
});
$(".nav-account-user").click(function (e) {
e.preventDefault();
$('.nav-account .wrapper-nav-sub').addClass('is-shown');
$('.nav-account .nav-account-user .title').addClass('is-selected');
});
$("#email-switch").change(function(e) {
var isEnabled = this.checked ? true : false,
switchLabel = gettext("OFF"),
headers = {
'X-CSRFToken': Cookies.get('course_discovery_csrftoken')
};
e.preventDefault();
$.ajax({
url: "/publisher/user/toggle/email_settings/",
type: "POST",
data: {is_enabled: isEnabled},
headers: headers,
success: function (response) {
if (response.is_enabled) {
switchLabel = gettext("ON")
}
$(".switch-label").html(switchLabel);
}
});
});
});
......@@ -15,34 +15,3 @@ function addDatePicker() {
}
});
}
$(document).ready(function () {
$("body").click(function() {
$('.nav-account .wrapper-nav-sub').removeClass('is-shown');
$('.nav-account .nav-account-user .title').removeClass('is-selected');
});
$('.nav-account .nav-item').click(function(e) {
$subnav = $('.nav-account').find('.wrapper-nav-sub');
$title = $(this).find('.title');
if ($subnav.hasClass('is-shown')) {
$subnav.removeClass('is-shown');
$title.removeClass('is-selected');
} else {
$('.nav-account .nav-item .title').removeClass('is-selected');
$('.nav-account .wrapper-nav-sub').removeClass('is-shown');
$title.addClass('is-selected');
$subnav.addClass('is-shown');
// if propagation is not stopped, the event will bubble up to the
// body element, which will close the dropdown.
e.stopPropagation();
}
});
$(".nav-account-user").click(function (e) {
e.preventDefault();
$('.nav-account .wrapper-nav-sub').addClass('is-shown');
$('.nav-account .nav-account-user .title').addClass('is-selected');
});
});
......@@ -46,6 +46,7 @@ $light-gray: rgba(204, 204, 204, 1);
overflow: visible;
height: auto;
position: absolute;
width: 340px;
&.is-shown {
display: block;
......@@ -101,6 +102,69 @@ $light-gray: rgba(204, 204, 204, 1);
width: 100%;
}
}
.on-off-text {
@include text-align(left);
}
.nav-email-notification {
.email-switch-label {
@include margin-right(60px);
}
.switch {
@include margin-right(10px);
position: relative;
display: inline-block;
width: 36px;
height: 20px;
vertical-align: middle;
}
.switch input {
display:none;
}
.slider {
@include left(0);
@include right(0);
position: absolute;
cursor: pointer;
top: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
border-radius: 20px;
}
.slider:before {
@include left(2px);
position: absolute;
content: "";
height: 16px;
width: 16px;
bottom: 2px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #333;
}
input:focus + .slider {
box-shadow: 0 0 1px #333;
}
input:checked + .slider:before {
-webkit-transform: translateX(16px);
-ms-transform: translateX(16px);
transform: translateX(16px);
}
}
}
}
......
......@@ -11,10 +11,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock title %} | {{ platform_name }}</title>
<script type="text/javascript" language="javascript" src="//code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="{% static 'bower_components/js-cookie/src/js.cookie.js' %}"></script>
<script src="{% static 'bower_components/underscore/underscore.js' %}"></script>
<script src="{% static 'bower_components/moment/moment.js' %}"></script>
<script src="{% static 'bower_components/pikaday/pikaday.js' %}"></script>
<script src="{% static 'js/publisher/views/navbar.js' %}"></script>
<script src="{% static 'js/utils.js' %}"></script>
<script src="{% url 'javascript-catalog' %}"></script>
<script type="text/javascript">
$(document).ready(function () {
addDatePicker();
......
......@@ -37,7 +37,12 @@
<div class="wrapper wrapper-nav-sub">
<div class="nav-sub">
<div class="nav-menu-item nav-email-notification">
<span><b>{% trans "Email notifications" %}</b></span>
<span class="email-switch-label"><b>{% trans "Email notifications" %}</b></span>
<label class="switch">
<input id="email-switch" type="checkbox" data-isEnabled="{{ is_email_notification_enabled }}" {% if is_email_notification_enabled %}checked{% endif %}>
<div class="slider"></div>
</label>
<span class="switch-label">{% if is_email_notification_enabled %} {% trans "ON" %} {% else %} {% trans "OFF" %} {% endif %}</span>
</div>
<div class="nav-menu-item nav-account-signout">
<a class="btn-neutral" href="/logout">{% trans "Sign Out" %}</a>
......
......@@ -43,6 +43,8 @@ urlpatterns = auth_urlpatterns + [
)
),
url(r'^comments/', include('django_comments.urls')),
url(r'^i18n/', include('django.conf.urls.i18n')),
url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', name='javascript-catalog'),
]
if settings.DEBUG: # pragma: no cover
......
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