Commit 77a796e2 by Jim Abramson

Merge pull request #520 from edx/feature/kevin/email_notifications_panel

Feature/kevin/email notifications panel
parents 0d10a872 05f31f8b
......@@ -20,10 +20,13 @@ if Backbone?
allThreads: ->
@nav.updateSidebar()
@nav.goHome()
setActiveThread: =>
if @thread
@nav.setActiveThread(@thread.get("id"))
else
@nav.goHome
showThread: (forum_name, thread_id) ->
@thread = @discussion.get(thread_id)
......
......@@ -82,6 +82,9 @@ class @DiscussionUtil
user_profile : "/courses/#{$$course_id}/discussion/forum/users/#{param}"
followed_threads : "/courses/#{$$course_id}/discussion/forum/users/#{param}/followed"
threads : "/courses/#{$$course_id}/discussion/forum"
"enable_notifications" : "/notification_prefs/enable/"
"disable_notifications" : "/notification_prefs/disable/"
"notifications_status" : "/notification_prefs/status"
}[name]
@safeAjax: (params) ->
......
......@@ -2,6 +2,7 @@ if Backbone?
class @DiscussionThreadListView extends Backbone.View
events:
"click .search": "showSearch"
"click .home": "goHome"
"click .browse": "toggleTopicDrop"
"keydown .post-search-field": "performSearch"
"click .sort-bar a": "sortThreads"
......@@ -43,6 +44,7 @@ if Backbone?
if active
@setActiveThread(thread_id)
#TODO fix this entire chain of events
addAndSelectThread: (thread) =>
commentable_id = thread.get("commentable_id")
......@@ -191,6 +193,25 @@ if Backbone?
@$(".browse").removeClass('is-open')
setTimeout (-> @$(".post-search-field").focus()), 200
goHome: ->
@template = _.template($("#discussion-home").html())
$(".discussion-column").html(@template)
$(".post-list a").removeClass("active")
$("input.email-setting").bind "click", @updateEmailNotifications
url = DiscussionUtil.urlFor("notifications_status",window.user.get("id"))
DiscussionUtil.safeAjax
url: url
type: "GET"
success: (response, textStatus) =>
if response.status
$('input.email-setting').attr('checked','checked')
else
$('input.email-setting').removeAttr('checked')
thread_id = null
@trigger("thread:removed")
#select all threads
toggleTopicDrop: (event) =>
event.preventDefault()
event.stopPropagation()
......@@ -312,6 +333,7 @@ if Backbone?
if callback?
callback()
retrieveDiscussions: (discussion_ids) ->
@discussionIds = discussion_ids.join(',')
@mode = 'commentables'
......@@ -418,3 +440,19 @@ if Backbone?
retrieveFollowed: (event)=>
@mode = 'followed'
@retrieveFirstPage(event)
updateEmailNotifications: () =>
if $('input.email-setting').attr('checked')
DiscussionUtil.safeAjax
url: DiscussionUtil.urlFor("enable_notifications")
type: "POST"
error: () =>
$('input.email-setting').removeAttr('checked')
else
DiscussionUtil.safeAjax
url: DiscussionUtil.urlFor("disable_notifications")
type: "POST"
error: () =>
$('input.email-setting').attr('checked','checked')
......@@ -17,7 +17,6 @@ urlpatterns = patterns('django_comment_client.base.views', # nopep8
url(r'threads/(?P<thread_id>[\w\-]+)/follow$', 'follow_thread', name='follow_thread'),
url(r'threads/(?P<thread_id>[\w\-]+)/unfollow$', 'unfollow_thread', name='unfollow_thread'),
url(r'threads/(?P<thread_id>[\w\-]+)/close$', 'openclose_thread', name='openclose_thread'),
url(r'comments/(?P<comment_id>[\w\-]+)/update$', 'update_comment', name='update_comment'),
url(r'comments/(?P<comment_id>[\w\-]+)/endorse$', 'endorse_comment', name='endorse_comment'),
url(r'comments/(?P<comment_id>[\w\-]+)/reply$', 'create_sub_comment', name='create_sub_comment'),
......
......@@ -387,7 +387,8 @@ def safe_content(content):
'updated_at', 'depth', 'type', 'commentable_id', 'comments_count',
'at_position_list', 'children', 'highlighted_title', 'highlighted_body',
'courseware_title', 'courseware_url', 'tags', 'unread_comments_count',
'read', 'group_id', 'group_name', 'group_string', 'pinned', 'abuse_flaggers'
'read', 'group_id', 'group_name', 'group_string', 'pinned', 'abuse_flaggers',
'stats'
]
......
import json
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import PermissionDenied
from django.http import Http404
......@@ -7,7 +9,7 @@ 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, unsubscribe
from notification_prefs.views import ajax_enable, ajax_disable, ajax_status, unsubscribe
from student.tests.factories import UserFactory
from user_api.models import UserPreference
......@@ -57,6 +59,34 @@ class NotificationPrefViewTest(TestCase):
UserPreference.objects.filter(user=user, key=NOTIFICATION_PREF_KEY).exists()
)
# AJAX status view
def test_ajax_status_get_0(self):
request = self.request_factory.get("dummy")
request.user = self.user
response = ajax_status(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content), {"status":0})
def test_ajax_status_get_1(self):
self.create_prefs()
request = self.request_factory.get("dummy")
request.user = self.user
response = ajax_status(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content), {"status":1})
def test_ajax_status_post(self):
request = self.request_factory.post("dummy")
request.user = self.user
response = ajax_status(request)
self.assertEqual(response.status_code, 405)
def test_ajax_status_anon_user(self):
request = self.request_factory.get("dummy")
request.user = AnonymousUser()
self.assertRaises(PermissionDenied, ajax_status, request)
# AJAX enable view
def test_ajax_enable_get(self):
......
from base64 import urlsafe_b64encode, urlsafe_b64decode
from hashlib import sha256
import json
from Crypto.Cipher import AES
from Crypto import Random
......@@ -131,6 +132,25 @@ def ajax_disable(request):
return HttpResponse(status=204)
@require_GET
def ajax_status(request):
"""
A view that retrieves notifications status for the authenticated user.
This view should be invoked by an AJAX GET call. It returns status 200,
with a JSON-formatted payload, or an error.
"""
if not request.user.is_authenticated():
raise PermissionDenied
qs = UserPreference.objects.filter(
user=request.user,
key=NOTIFICATION_PREF_KEY
)
return HttpResponse(json.dumps({"status":len(qs)}), content_type="application/json")
@require_GET
def unsubscribe(request, token):
......
......@@ -182,6 +182,9 @@ COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", [])
MITX_FEATURES['AUTOMATIC_AUTH_FOR_LOAD_TESTING'] = ENV_TOKENS.get('AUTOMATIC_AUTH_FOR_LOAD_TESTING')
MITX_FEATURES['MAX_AUTO_AUTH_USERS'] = ENV_TOKENS.get('MAX_AUTO_AUTH_USERS')
# discussion home panel must be explicitly enabled
MITX_FEATURES['ENABLE_DISCUSSION_HOME_PANEL'] = ENV_TOKENS.get('ENABLE_DISCUSSION_HOME_PANEL', False)
############################## SECURE AUTH ITEMS ###############
# Secret things: passwords, access keys, etc.
......
......@@ -71,6 +71,9 @@ MITX_FEATURES = {
'ENABLE_TEXTBOOK': True,
'ENABLE_DISCUSSION_SERVICE': True,
# discussion home panel, which includes a subscription on/off setting for discussion digest emails.
# this should remain off in production until digest notifications are online.
'ENABLE_DISCUSSION_HOME_PANEL': True,
'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard)
......
......@@ -102,3 +102,8 @@ def _url_for_user_active_threads(user_id):
def _url_for_user_subscribed_threads(user_id):
return "{prefix}/users/{user_id}/subscribed_threads".format(prefix=settings.PREFIX, user_id=user_id)
def _url_for_user_stats(user_id,course_id):
return "{prefix}/users/{user_id}/stats?course_id={course_id}".format(prefix=settings.PREFIX, user_id=user_id,course_id=course_id)
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -68,7 +68,10 @@
width: 16px;
height: 17px;
margin: 7px 7px 0 0;
background: url(../images/new-post-icon.png) no-repeat;
font-size: 16px;
padding-right: $baseline/2;
vertical-align: middle;
color: $white;
}
.post-search {
......
@import 'bourbon/bourbon';
// lms - css application architecture
// ====================
// libs and resets *do not edit*
@import 'bourbon/bourbon'; // lib - bourbon
// VENDOR + REBASE *referenced/used vendor presentation and reset*
// ====================
@import 'base/reset';
@import 'base/font_face';
@import 'vendor/font-awesome';
// BASE *default edX offerings*
// ====================
// base - utilities
@import 'base/mixins';
@import 'base/variables';
......@@ -19,10 +32,16 @@
% endif
@import 'base/base';
// base - assets
@import 'base/font_face';
@import 'base/extends';
@import 'base/animations';
@import 'base/animations';
// base - starter
@import 'base/base';
// Multicourse styles
// shared - course
@import 'shared/forms';
@import 'shared/footer';
@import 'shared/header';
......@@ -32,6 +51,7 @@
@import 'shared/activation_messages';
@import 'shared/unsubscribe';
// shared - platform
@import 'multicourse/home';
@import 'multicourse/dashboard';
@import 'multicourse/account';
......@@ -47,7 +67,8 @@
@import 'multicourse/help';
@import 'multicourse/edge';
// applications
@import 'discussion';
@import 'news';
@import 'shame';
@import 'shame'; // shame file - used for any bad-form/orphaned scss that knowingly violate edX FED architecture/standards (see - http://csswizardry.com/2013/04/shame-css/)
......@@ -34,6 +34,7 @@ $dark-gray: rgb(51, 51, 51);
$border-color: rgb(200, 200, 200);
$sidebar-color: rgb(246, 246, 246);
$outer-border-color: rgb(170, 170, 170);
$green: rgb(37, 184, 90);
// old variables
$light-gray: #ddd;
......@@ -195,4 +196,4 @@ $homepage-bg-image: '../images/homepage-bg.jpg';
$login-banner-image: url(../images/bg-banner-login.png);
$register-banner-image: url(../images/bg-banner-register.png);
$video-thumb-url: '../images/courses/video-thumb.jpg';
$video-thumb-url: '../images/courses/video-thumb.jpg';
\ No newline at end of file
......@@ -3,6 +3,6 @@
<%block name="extratabs">
% if has_permission(user, 'create_thread', course.id):
<li class="right"><a href="#" class="new-post-btn"><span class="new-post-icon"></span>New Post</a></li>
<li class="right"><a href="#" class="new-post-btn"><span class="icon icon-edit new-post-icon"></span>New Post</a></li>
% endif
</%block>
\ No newline at end of file
......@@ -2,7 +2,7 @@
<div class="discussion-module" data-discussion-id="${discussion_id | h}">
<a class="discussion-show control-button" href="javascript:void(0)" data-discussion-id="${discussion_id | h}"><span class="show-hide-discussion-icon"></span><span class="button-text">Show Discussion</span></a>
<a href="#" class="new-post-btn"><span class="new-post-icon"></span>New Post</a>
<a href="#" class="new-post-btn"><span class="icon icon-edit new-post-icon"></span>New Post</a>
</div>
......@@ -11,12 +11,12 @@
</%def>
<%def name="render_entry(entries, entry)">
<li><a href="#"><span class="board-name" data-discussion_id='${json.dumps(entries[entry])}' cohorted = "${entries[entry]['id'] in cohorted_commentables}">${entry}</span></a></li>
<li><a href="#" class="drop-menu-entry"><span class="board-name" data-discussion_id='${json.dumps(entries[entry])}' cohorted = "${entries[entry]['id'] in cohorted_commentables}">${entry}</span></a></li>
</%def>
<%def name="render_category(categories, category)">
<li>
<a href="#"><span class="board-name">${category}</span></a>
<a href="#" class="drop-menu-parent-category"><span class="board-name">${category}</span></a>
<ul>
${render_dropdown(categories[category])}
</ul>
......@@ -29,21 +29,21 @@
</div>
<ul class="browse-topic-drop-menu">
<li>
<a href="#">
<a href="#" class="drop-menu-meta-category">
<span class="board-name" data-discussion_id='#all'>Show All Discussions</span>
</a>
</li>
%if flag_moderator:
<li>
<a href="#">
<span class="board-name" data-discussion_id='#flagged'>Show Flagged Discussions</span>
<span class="board-name" data-discussion_id='#flagged'><i class="icon-flag" style="padding-right:5px;"></i>Show Flagged Discussions</span>
</a>
</li>
%endif
<li>
<a href="#">
<span class="board-name" data-discussion_id='#following'>Following</span>
<a href="#" class="drop-menu-meta-category">
<span class="board-name" data-discussion_id='#following'><i class="icon-star" style="padding-right:5px;"></i>Posts I&#39;m Following</span>
</a>
</li>
${render_dropdown(category_map)}
......
<script type="text/template" id="thread-list-template">
<div class="browse-search">
<div class="home">
<a href="#" class="home-icon">
<i class="icon icon-home"></i>
</a>
</div>
<div class="browse is-open">
<a href="#" class="browse-topic-drop-icon"></a>
<a href="#" class="browse-topic-drop-icon">
<i class="icon icon-reorder"></i>
</a>
<a href="#" class="browse-topic-drop-btn"><span class="current-board">Show All Discussions</span> <span class="drop-arrow">▾</span></a>
</div>
<%include file="_filter_dropdown.html" />
......
......@@ -47,18 +47,18 @@
</header>
<div class="post-body">${'<%- body %>'}</div>
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" data-tooltip="Report Misuse">
<i class="icon"></i><span class="flag-label">Report Misuse</span></div>
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" data-tooltip="Report Misuse">
<i class="icon icon-flag"></i><span class="flag-label">Report Misuse</span></div>
% if course and has_permission(user, 'openclose_thread', course.id):
<div class="admin-pin discussion-pin notpinned" data-role="thread-pin" data-tooltip="pin this thread">
<i class="icon"></i><span class="pin-label">Pin Thread</span></div>
<div class="admin-pin discussion-pin notpinned" data-role="thread-pin" data-tooltip="pin this thread">
<i class="icon icon-pushpin"></i><span class="pin-label">Pin Thread</span></div>
%else:
${"<% if (pinned) { %>"}
<div class="discussion-pin notpinned" data-role="thread-pin" data-tooltip="pin this thread">
<i class="icon"></i><span class="pin-label">Pin Thread</span></div>
<div class="discussion-pin notpinned" data-role="thread-pin" data-tooltip="pin this thread">
<i class="icon icon-pushpin"></i><span class="pin-label">Pin Thread</span></div>
${"<% } %>"}
% endif
......@@ -126,7 +126,7 @@
</header>
<div class="response-local"><div class="response-body">${"<%- body %>"}</div>
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" data-tooltip="report misuse">
<i class="icon"></i><span class="flag-label">Report Misuse</span></div>
<i class="icon icon-flag"></i><span class="flag-label">Report Misuse</span></div>
</div>
<ul class="moderator-actions response-local">
<li style="display: none"><a class="action-edit" href="javascript:void(0)"><span class="edit-icon"></span> Edit</a></li>
......@@ -150,8 +150,8 @@
<script type="text/template" id="response-comment-show-template">
<div id="comment_${'<%- id %>'}">
<div class="response-body">${'<%- body %>'}</div>
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" data-tooltip="report misuse">
<i class="icon"></i><span class="flag-label"></span></div>
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" data-tooltip="Report Misuse">
<i class="icon icon-flag"></i><span class="flag-label"></span></div>
<p class="posted-details">&ndash;posted <span class="timeago" title="${'<%- created_at %>'}">${'<%- created_at %>'}</span> by
${"<% if (obj.username) { %>"}
<a href="${'<%- user_url %>'}" class="profile-link">${'<%- username %>'}</a>
......@@ -171,3 +171,56 @@
<span class="votes-count">+${"<%- votes['up_count'] %>"}</span>
</a>
</script>
<script type="text/template" id="discussion-home">
<div class="discussion-article blank-slate">
<section class="home-header">
<span class="label">DISCUSSION HOME:</span>
<h1 class="home-title">${course.display_name_with_default}</h1>
</section>
% if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_HOME_PANEL'):
<span class="label label-settings">HOW TO USE EDX DISCUSSIONS</span>
<table class="home-helpgrid">
<tr class="helpgrid-row helpgrid-row-navigation">
<td class="row-title">Find discussions</td>
<td class="row-item">
<i class="icon icon-reorder"></i>
<span class="row-description">Focus in on specific topics</span>
</td>
<td class="row-item">
<i class="icon icon-search"></i>
<span class="row-description">Search for specific posts </span>
</td>
<td class="row-item">
<i class="icon icon-sort"></i>
<span class="row-description">Sort by date, vote, or comments </span>
</td>
</tr>
<tr class="helpgrid-row helpgrid-row-participation">
<td class="row-title">Engage with posts</td>
<td class="row-item">
<i class="icon icon-plus"></i>
<span class="row-description">Upvote posts and good responses</span>
</td>
<td class="row-item">
<i class="icon icon-flag"></i>
<span class="row-description">Report Forum Misuse</span>
</td>
<td class="row-item">
<i class="icon icon-star"></i>
<span class="row-description">Follow posts for updates</span>
</td>
</tr>
<tr class="helpgrid-row helpgrid-row-notification">
<td class="row-title">Receive updates</td>
<td class="row-item-full" colspan="3">
<input type="checkbox" class="email-setting" name="email-notification"></input>
<i class="icon icon-envelope"></i>
<span class="row-description"> If enabled, you will receive an email digest once a day notifying you about new, unread activity from posts you are following. </span>
</td>
</tr>
</table>
% endif
</div>
</script>
......@@ -9,7 +9,7 @@
<%block name="headextra">
<%static:css group='course'/>
<%include file="_js_head_dependencies.html" />
<%include file="_js_head_dependencies.html" />
</%block>
<%block name="js_extra">
......@@ -25,13 +25,9 @@
<div class="discussion-body">
<div class="sidebar"></div>
<div class="discussion-column">
<div class="discussion-article blank-slate">
<h1>${course.display_name_with_default} Discussion</h1>
</div>
</div>
</div>
</section>
</section>
<%include file="_underscore_templates.html" />
<%include file="_thread_list_template.html" />
......@@ -4,10 +4,10 @@
<a href="#" class="vote-btn discussion-vote discussion-vote-up" data-role="discussion-vote" data-tooltip="vote"><span class="plus-icon">+</span> <span class='votes-count-number'>{{votes.up_count}}</span></a>
<h3>{{title}}</h3>
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" data-tooltip="Report Misuse">
<i class="icon"></i><span class="flag-label">Flagged</span></div>
<i class="icon icon-flag"></i><span class="flag-label">Flagged</span></div>
<div class="discussion-pin-inline pinned pinned-{{pinned}}" data-tooltip="This thread has been pinned by course staff.">
<i class="icon"></i><span class="pin-label">Pinned</span></div>
<i class="icon icon-pushpin"></i><span class="pin-label">Pinned</span></div>
<p class="posted-details">
{{#user}}
......
......@@ -336,6 +336,7 @@ if settings.COURSEWARE_ENABLED:
include('django_comment_client.urls')),
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<token>[a-zA-Z0-9-_=]+)/', 'notification_prefs.views.unsubscribe'),
)
urlpatterns += (
......
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