Commit 753cc9ec by David Ormsbee

Merge pull request #1666 from MITx/feature/kevin/pinning

Feature/kevin/pinning
parents 55a9bba1 8d766c4b
...@@ -29,4 +29,5 @@ cover_html/ ...@@ -29,4 +29,5 @@ cover_html/
.idea/ .idea/
.redcar/ .redcar/
chromedriver.log chromedriver.log
/nbproject
ghostdriver.log ghostdriver.log
...@@ -79,6 +79,17 @@ if Backbone? ...@@ -79,6 +79,17 @@ if Backbone?
@getContent(id).updateInfo(info) @getContent(id).updateInfo(info)
$.extend @contentInfos, infos $.extend @contentInfos, infos
pinThread: ->
pinned = @get("pinned")
@set("pinned",pinned)
@trigger "change", @
unPinThread: ->
pinned = @get("pinned")
@set("pinned",pinned)
@trigger "change", @
class @Thread extends @Content class @Thread extends @Content
urlMappers: urlMappers:
'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id) 'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id)
...@@ -91,6 +102,8 @@ if Backbone? ...@@ -91,6 +102,8 @@ if Backbone?
'delete' : -> DiscussionUtil.urlFor('delete_thread', @id) 'delete' : -> DiscussionUtil.urlFor('delete_thread', @id)
'follow' : -> DiscussionUtil.urlFor('follow_thread', @id) 'follow' : -> DiscussionUtil.urlFor('follow_thread', @id)
'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id) 'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id)
'pinThread' : -> DiscussionUtil.urlFor("pin_thread", @id)
'unPinThread' : -> DiscussionUtil.urlFor("un_pin_thread", @id)
initialize: -> initialize: ->
@set('thread', @) @set('thread', @)
......
...@@ -58,9 +58,30 @@ if Backbone? ...@@ -58,9 +58,30 @@ if Backbone?
@current_page = response.page @current_page = response.page
sortByDate: (thread) -> sortByDate: (thread) ->
#
#The comment client asks each thread for a value by which to sort the collection
#and calls this sort routine regardless of the order returned from the LMS/comments service
#so, this takes advantage of this per-thread value and returns tomorrow's date
#for pinned threads, ensuring that they appear first, (which is the intent of pinned threads)
#
if thread.get('pinned')
#use tomorrow's date
today = new Date();
new Date(today.getTime() + (24 * 60 * 60 * 1000));
else
thread.get("created_at") thread.get("created_at")
sortByDateRecentFirst: (thread) -> sortByDateRecentFirst: (thread) ->
#
#Same as above
#but negative to flip the order (newest first)
#
if thread.get('pinned')
#use tomorrow's date
today = new Date();
-(new Date(today.getTime() + (24 * 60 * 60 * 1000)));
else
-(new Date(thread.get("created_at")).getTime()) -(new Date(thread.get("created_at")).getTime())
#return String.fromCharCode.apply(String, #return String.fromCharCode.apply(String,
# _.map(thread.get("created_at").split(""), # _.map(thread.get("created_at").split(""),
......
...@@ -50,6 +50,8 @@ class @DiscussionUtil ...@@ -50,6 +50,8 @@ class @DiscussionUtil
delete_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/delete" delete_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/delete"
upvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/upvote" upvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/upvote"
downvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/downvote" downvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/downvote"
pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/pin"
un_pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unpin"
undo_vote_for_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unvote" undo_vote_for_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unvote"
follow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/follow" follow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/follow"
unfollow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unfollow" unfollow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unfollow"
......
...@@ -3,6 +3,7 @@ if Backbone? ...@@ -3,6 +3,7 @@ if Backbone?
events: events:
"click .discussion-vote": "toggleVote" "click .discussion-vote": "toggleVote"
"click .admin-pin": "togglePin"
"click .action-follow": "toggleFollowing" "click .action-follow": "toggleFollowing"
"click .action-edit": "edit" "click .action-edit": "edit"
"click .action-delete": "delete" "click .action-delete": "delete"
...@@ -24,6 +25,7 @@ if Backbone? ...@@ -24,6 +25,7 @@ if Backbone?
@delegateEvents() @delegateEvents()
@renderDogear() @renderDogear()
@renderVoted() @renderVoted()
@renderPinned()
@renderAttrs() @renderAttrs()
@$("span.timeago").timeago() @$("span.timeago").timeago()
@convertMath() @convertMath()
...@@ -41,8 +43,20 @@ if Backbone? ...@@ -41,8 +43,20 @@ if Backbone?
else else
@$("[data-role=discussion-vote]").removeClass("is-cast") @$("[data-role=discussion-vote]").removeClass("is-cast")
renderPinned: =>
if @model.get("pinned")
@$("[data-role=thread-pin]").addClass("pinned")
@$("[data-role=thread-pin]").removeClass("notpinned")
@$(".discussion-pin .pin-label").html("Pinned")
else
@$("[data-role=thread-pin]").removeClass("pinned")
@$("[data-role=thread-pin]").addClass("notpinned")
@$(".discussion-pin .pin-label").html("Pin Thread")
updateModelDetails: => updateModelDetails: =>
@renderVoted() @renderVoted()
@renderPinned()
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"]) @$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"])
convertMath: -> convertMath: ->
...@@ -99,6 +113,34 @@ if Backbone? ...@@ -99,6 +113,34 @@ if Backbone?
delete: (event) -> delete: (event) ->
@trigger "thread:delete", event @trigger "thread:delete", event
togglePin: (event) ->
event.preventDefault()
if @model.get('pinned')
@unPin()
else
@pin()
pin: ->
url = @model.urlFor("pinThread")
DiscussionUtil.safeAjax
$elem: @$(".discussion-pin")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set('pinned', true)
unPin: ->
url = @model.urlFor("unPinThread")
DiscussionUtil.safeAjax
$elem: @$(".discussion-pin")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set('pinned', false)
toggleClosed: (event) -> toggleClosed: (event) ->
$elem = $(event.target) $elem = $(event.target)
url = @model.urlFor('close') url = @model.urlFor('close')
...@@ -137,3 +179,5 @@ if Backbone? ...@@ -137,3 +179,5 @@ if Backbone?
if @model.get('username')? if @model.get('username')?
params = $.extend(params, user:{username: @model.username, user_url: @model.user_url}) params = $.extend(params, user:{username: @model.username, user_url: @model.user_url})
Mustache.render(@template, params) Mustache.render(@template, params)
\ No newline at end of file
...@@ -12,6 +12,8 @@ urlpatterns = patterns('django_comment_client.base.views', ...@@ -12,6 +12,8 @@ urlpatterns = patterns('django_comment_client.base.views',
url(r'threads/(?P<thread_id>[\w\-]+)/upvote$', 'vote_for_thread', {'value': 'up'}, name='upvote_thread'), url(r'threads/(?P<thread_id>[\w\-]+)/upvote$', 'vote_for_thread', {'value': 'up'}, name='upvote_thread'),
url(r'threads/(?P<thread_id>[\w\-]+)/downvote$', 'vote_for_thread', {'value': 'down'}, name='downvote_thread'), url(r'threads/(?P<thread_id>[\w\-]+)/downvote$', 'vote_for_thread', {'value': 'down'}, name='downvote_thread'),
url(r'threads/(?P<thread_id>[\w\-]+)/unvote$', 'undo_vote_for_thread', name='undo_vote_for_thread'), url(r'threads/(?P<thread_id>[\w\-]+)/unvote$', 'undo_vote_for_thread', name='undo_vote_for_thread'),
url(r'threads/(?P<thread_id>[\w\-]+)/pin$', 'pin_thread', name='pin_thread'),
url(r'threads/(?P<thread_id>[\w\-]+)/unpin$', 'un_pin_thread', name='un_pin_thread'),
url(r'threads/(?P<thread_id>[\w\-]+)/follow$', 'follow_thread', name='follow_thread'), 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\-]+)/unfollow$', 'unfollow_thread', name='unfollow_thread'),
url(r'threads/(?P<thread_id>[\w\-]+)/close$', 'openclose_thread', name='openclose_thread'), url(r'threads/(?P<thread_id>[\w\-]+)/close$', 'openclose_thread', name='openclose_thread'),
......
...@@ -289,6 +289,21 @@ def undo_vote_for_thread(request, course_id, thread_id): ...@@ -289,6 +289,21 @@ def undo_vote_for_thread(request, course_id, thread_id):
user.unvote(thread) user.unvote(thread)
return JsonResponse(utils.safe_content(thread.to_dict())) return JsonResponse(utils.safe_content(thread.to_dict()))
@require_POST
@login_required
@permitted
def pin_thread(request, course_id, thread_id):
user = cc.User.from_django_user(request.user)
thread = cc.Thread.find(thread_id)
thread.pin(user,thread_id)
return JsonResponse(utils.safe_content(thread.to_dict()))
def un_pin_thread(request, course_id, thread_id):
user = cc.User.from_django_user(request.user)
thread = cc.Thread.find(thread_id)
thread.un_pin(user,thread_id)
return JsonResponse(utils.safe_content(thread.to_dict()))
@require_POST @require_POST
@login_required @login_required
......
...@@ -91,6 +91,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG ...@@ -91,6 +91,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
#now add the group name if the thread has a group id #now add the group name if the thread has a group id
for thread in threads: for thread in threads:
if thread.get('group_id'): if thread.get('group_id'):
thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name
thread['group_string'] = "This post visible only to Group %s." % (thread['group_name']) thread['group_string'] = "This post visible only to Group %s." % (thread['group_name'])
...@@ -210,6 +211,9 @@ def forum_form_discussion(request, course_id): ...@@ -210,6 +211,9 @@ def forum_form_discussion(request, course_id):
user_cohort_id = get_cohort_id(request.user, course_id) user_cohort_id = get_cohort_id(request.user, course_id)
context = { context = {
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
'course': course, 'course': course,
......
...@@ -90,6 +90,8 @@ VIEW_PERMISSIONS = { ...@@ -90,6 +90,8 @@ VIEW_PERMISSIONS = {
'undo_vote_for_comment': [['unvote', 'is_open']], 'undo_vote_for_comment': [['unvote', 'is_open']],
'vote_for_thread' : [['vote', 'is_open']], 'vote_for_thread' : [['vote', 'is_open']],
'undo_vote_for_thread': [['unvote', 'is_open']], 'undo_vote_for_thread': [['unvote', 'is_open']],
'pin_thread': ['create_comment'],
'un_pin_thread': ['create_comment'],
'follow_thread' : ['follow_thread'], 'follow_thread' : ['follow_thread'],
'follow_commentable': ['follow_commentable'], 'follow_commentable': ['follow_commentable'],
'follow_user' : ['follow_user'], 'follow_user' : ['follow_user'],
......
...@@ -406,7 +406,7 @@ def safe_content(content): ...@@ -406,7 +406,7 @@ def safe_content(content):
'updated_at', 'depth', 'type', 'commentable_id', 'comments_count', 'updated_at', 'depth', 'type', 'commentable_id', 'comments_count',
'at_position_list', 'children', 'highlighted_title', 'highlighted_body', 'at_position_list', 'children', 'highlighted_title', 'highlighted_body',
'courseware_title', 'courseware_url', 'tags', 'unread_comments_count', 'courseware_title', 'courseware_url', 'tags', 'unread_comments_count',
'read', 'group_id', 'group_name', 'group_string' 'read', 'group_id', 'group_name', 'group_string', 'pinned'
] ]
if (content.get('anonymous') is False) and (content.get('anonymous_to_peers') is False): if (content.get('anonymous') is False) and (content.get('anonymous_to_peers') is False):
......
...@@ -11,12 +11,12 @@ class Thread(models.Model): ...@@ -11,12 +11,12 @@ class Thread(models.Model):
'closed', 'tags', 'votes', 'commentable_id', 'username', 'user_id', 'closed', 'tags', 'votes', 'commentable_id', 'username', 'user_id',
'created_at', 'updated_at', 'comments_count', 'unread_comments_count', 'created_at', 'updated_at', 'comments_count', 'unread_comments_count',
'at_position_list', 'children', 'type', 'highlighted_title', 'at_position_list', 'children', 'type', 'highlighted_title',
'highlighted_body', 'endorsed', 'read', 'group_id', 'group_name' 'highlighted_body', 'endorsed', 'read', 'group_id', 'group_name', 'pinned'
] ]
updatable_fields = [ updatable_fields = [
'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id',
'closed', 'tags', 'user_id', 'commentable_id', 'group_id', 'group_name' 'closed', 'tags', 'user_id', 'commentable_id', 'group_id', 'group_name', 'pinned'
] ]
initializable_fields = updatable_fields initializable_fields = updatable_fields
...@@ -79,3 +79,23 @@ class Thread(models.Model): ...@@ -79,3 +79,23 @@ class Thread(models.Model):
response = perform_request('get', url, request_params) response = perform_request('get', url, request_params)
self.update_attributes(**response) self.update_attributes(**response)
def pin(self, user, thread_id):
url = _url_for_pin_thread(thread_id)
params = {'user_id': user.id}
request = perform_request('put', url, params)
self.update_attributes(request)
def un_pin(self, user, thread_id):
url = _url_for_un_pin_thread(thread_id)
params = {'user_id': user.id}
request = perform_request('put', url, params)
self.update_attributes(request)
def _url_for_pin_thread(thread_id):
return "{prefix}/threads/{thread_id}/pin".format(prefix=settings.PREFIX, thread_id=thread_id)
def _url_for_un_pin_thread(thread_id):
return "{prefix}/threads/{thread_id}/unpin".format(prefix=settings.PREFIX, thread_id=thread_id)
\ No newline at end of file
...@@ -2443,3 +2443,38 @@ body.discussion { ...@@ -2443,3 +2443,38 @@ body.discussion {
font-style: italic; font-style: italic;
background-color:#fff; background-color:#fff;
} }
.discussion-pin {
font-size: 12px;
float:right;
padding-right: 5px;
font-style: italic;
}
.notpinned .icon
{
display: inline-block;
width: 10px;
height: 14px;
padding-right: 3px;
background: transparent url('../images/unpinned.png') no-repeat 0 0;
}
.pinned .icon
{
display: inline-block;
width: 10px;
height: 14px;
padding-right: 3px;
background: transparent url('../images/pinned.png') no-repeat 0 0;
}
.pinned span {
color: #B82066;
font-style: italic;
}
.notpinned span {
color: #888;
font-style: italic;
}
\ No newline at end of file
...@@ -45,6 +45,21 @@ ...@@ -45,6 +45,21 @@
</header> </header>
<div class="post-body">${'<%- body %>'}</div> <div class="post-body">${'<%- body %>'}</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>
%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>
${"<% } %>"}
% endif
${'<% if (obj.courseware_url) { %>'} ${'<% if (obj.courseware_url) { %>'}
<div class="post-context"> <div class="post-context">
(this post is about <a href="${'<%- courseware_url%>'}">${'<%- courseware_title %>'}</a>) (this post is about <a href="${'<%- courseware_url%>'}">${'<%- courseware_title %>'}</a>)
......
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