Commit 8be15d86 by David Ormsbee

Merge pull request #1683 from MITx/feature/kevin/pinning_patch

Feature/kevin/pinning patch
parents faab0ac1 53257a3c
...@@ -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
...@@ -310,11 +310,26 @@ class CapaModule(XModule): ...@@ -310,11 +310,26 @@ class CapaModule(XModule):
is_survey_question = (self.max_attempts == 0) is_survey_question = (self.max_attempts == 0)
needs_reset = self.is_completed() and self.rerandomize == "always" needs_reset = self.is_completed() and self.rerandomize == "always"
# If the student has unlimited attempts, and their answers
# are not randomized, then we do not need a save button
# because they can use the "Check" button without consequences.
#
# The consequences we want to avoid are:
# * Using up an attempt (if max_attempts is set)
# * Changing the current problem, and no longer being
# able to view it (if rerandomize is "always")
#
# In those cases. the if statement below is false,
# and the save button can still be displayed.
#
if self.max_attempts is None and self.rerandomize != "always":
return False
# If the problem is closed (and not a survey question with max_attempts==0), # If the problem is closed (and not a survey question with max_attempts==0),
# then do NOT show the reset button # then do NOT show the save button
# If we're waiting for the user to reset a randomized problem # If we're waiting for the user to reset a randomized problem
# then do NOT show the reset button # then do NOT show the save button
if (self.closed() and not is_survey_question) or needs_reset: elif (self.closed() and not is_survey_question) or needs_reset:
return False return False
else: else:
return True return True
...@@ -729,7 +744,7 @@ class CapaModule(XModule): ...@@ -729,7 +744,7 @@ class CapaModule(XModule):
event_info['answers'] = answers event_info['answers'] = answers
# Too late. Cannot submit # Too late. Cannot submit
if self.closed() and not self.max_attempts==0: if self.closed() and not self.max_attempts ==0:
event_info['failure'] = 'closed' event_info['failure'] = 'closed'
self.system.track_function('save_problem_fail', event_info) self.system.track_function('save_problem_fail', event_info)
return {'success': False, return {'success': False,
...@@ -747,7 +762,7 @@ class CapaModule(XModule): ...@@ -747,7 +762,7 @@ class CapaModule(XModule):
self.system.track_function('save_problem_success', event_info) self.system.track_function('save_problem_success', event_info)
msg = "Your answers have been saved" msg = "Your answers have been saved"
if not self.max_attempts==0: if not self.max_attempts ==0:
msg += " but not graded. Hit 'Check' to grade them." msg += " but not graded. Hit 'Check' to grade them."
return {'success': True, return {'success': True,
'msg': msg} 'msg': msg}
...@@ -793,7 +808,7 @@ class CapaModule(XModule): ...@@ -793,7 +808,7 @@ class CapaModule(XModule):
event_info['new_state'] = self.lcp.get_state() event_info['new_state'] = self.lcp.get_state()
self.system.track_function('reset_problem', event_info) self.system.track_function('reset_problem', event_info)
return { 'success': True, return {'success': True,
'html': self.get_problem_html(encapsulate=False)} 'html': self.get_problem_html(encapsulate=False)}
...@@ -821,13 +836,13 @@ class CapaDescriptor(RawDescriptor): ...@@ -821,13 +836,13 @@ class CapaDescriptor(RawDescriptor):
def get_context(self): def get_context(self):
_context = RawDescriptor.get_context(self) _context = RawDescriptor.get_context(self)
_context.update({'markdown': self.metadata.get('markdown', ''), _context.update({'markdown': self.metadata.get('markdown', ''),
'enable_markdown' : 'markdown' in self.metadata}) 'enable_markdown': 'markdown' in self.metadata})
return _context return _context
@property @property
def editable_metadata_fields(self): def editable_metadata_fields(self):
"""Remove any metadata from the editable fields which have their own editor or shouldn't be edited by user.""" """Remove any metadata from the editable fields which have their own editor or shouldn't be edited by user."""
subset = [field for field in super(CapaDescriptor,self).editable_metadata_fields subset = [field for field in super(CapaDescriptor, self).editable_metadata_fields
if field not in ['markdown', 'empty']] if field not in ['markdown', 'empty']]
return subset return subset
......
...@@ -87,7 +87,7 @@ class FolditModule(XModule): ...@@ -87,7 +87,7 @@ class FolditModule(XModule):
from foldit.models import Score from foldit.models import Score
leaders = [(e['username'], e['score']) for e in Score.get_tops_n(10)] leaders = [(e['username'], e['score']) for e in Score.get_tops_n(10)]
leaders.sort(key=lambda x: x[1]) leaders.sort(key=lambda x: -x[1])
return leaders return leaders
......
...@@ -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,36 @@ if Backbone? ...@@ -99,6 +113,36 @@ 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)
error: =>
$('.admin-pin').text("Pinning not currently available")
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 +181,5 @@ if Backbone? ...@@ -137,3 +181,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
...@@ -57,10 +57,15 @@ pre, #dna-strand { ...@@ -57,10 +57,15 @@ pre, #dna-strand {
background: white; background: white;
} }
.gwt-DialogBox .dialogBottomCenter { .gwt-DialogBox .dialogBottomCenter {
background: url(images/hborder.png) repeat-x 0px -2945px;
-background: url(images/hborder_ie6.png) repeat-x 0px -2144px;
} }
.gwt-DialogBox .dialogMiddleLeft { .gwt-DialogBox .dialogMiddleLeft {
background: url(images/vborder.png) repeat-y -31px 0px;
} }
.gwt-DialogBox .dialogMiddleRight { .gwt-DialogBox .dialogMiddleRight {
background: url(images/vborder.png) repeat-y -32px 0px;
-background: url(images/vborder_ie6.png) repeat-y -32px 0px;
} }
.gwt-DialogBox .dialogTopLeftInner { .gwt-DialogBox .dialogTopLeftInner {
width: 10px; width: 10px;
...@@ -82,12 +87,20 @@ pre, #dna-strand { ...@@ -82,12 +87,20 @@ pre, #dna-strand {
zoom: 1; zoom: 1;
} }
.gwt-DialogBox .dialogTopLeft { .gwt-DialogBox .dialogTopLeft {
background: url(images/circles.png) no-repeat -20px 0px;
-background: url(images/circles_ie6.png) no-repeat -20px 0px;
} }
.gwt-DialogBox .dialogTopRight { .gwt-DialogBox .dialogTopRight {
background: url(images/circles.png) no-repeat -28px 0px;
-background: url(images/circles_ie6.png) no-repeat -28px 0px;
} }
.gwt-DialogBox .dialogBottomLeft { .gwt-DialogBox .dialogBottomLeft {
background: url(images/circles.png) no-repeat 0px -36px;
-background: url(images/circles_ie6.png) no-repeat 0px -36px;
} }
.gwt-DialogBox .dialogBottomRight { .gwt-DialogBox .dialogBottomRight {
background: url(images/circles.png) no-repeat -8px -36px;
-background: url(images/circles_ie6.png) no-repeat -8px -36px;
} }
* html .gwt-DialogBox .dialogTopLeftInner { * html .gwt-DialogBox .dialogTopLeftInner {
width: 10px; width: 10px;
......
function genex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='026A6180B5959B8660E084245FEE5E9E',Rb='1F433010E1134C95BF6CB43F552F3019',Sb='2DDA730EDABB80B88A6B0DFA3AFEACA2',Tb='4EEB1DCF4B30D366C27968D1B5C0BD04',Ub='5033ABB047340FB9346B622E2CC7107D',Wb=':',pb='::',dc='<script defer="defer">genex.onInjectionDone(\'genex\')<\/script>',hb='<script id="',sb='=',$='?',ub='Bad handler "',Vb='DF3D3A7FAEE63D711CF2D95BDB3F538C',cc='DOMContentLoaded',jb='SCRIPT',gb='__gwt_marker_genex',kb='base',cb='baseUrl',T='begin',S='bootstrap',bb='clear.cache.gif',rb='content',Y='end',Kb='gecko',Lb='gecko1_8',Q='genex',Yb='genex.css',eb='genex.nocache.js',ob='genex::',U='gwt.codesvr=',V='gwt.hosted=',W='gwt.hybrid',wb='gwt:onLoadErrorFn',tb='gwt:onPropertyErrorFn',qb='gwt:property',bc='head',Ob='hosted.html?genex',ac='href',Jb='ie6',Ib='ie8',Hb='ie9',yb='iframe',ab='img',zb="javascript:''",Zb='link',Nb='loadExternalRefs',mb='meta',Bb='moduleRequested',X='moduleStartup',Gb='msie',nb='name',Db='opera',Ab='position:absolute;width:0;height:0;border:none',$b='rel',Fb='safari',db='script',Pb='selectingPermutation',R='startup',_b='stylesheet',fb='undefined',Mb='unknown',Cb='user.agent',Eb='webkit';var m=window,n=document,o=m.__gwtStatsEvent?function(a){return m.__gwtStatsEvent(a)}:null,p=m.__gwtStatsSessionId?m.__gwtStatsSessionId:null,q,r,s,t=P,u={},v=[],w=[],x=[],y=0,z,A;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:T});if(!m.__gwt_stylesLoaded){m.__gwt_stylesLoaded={}}if(!m.__gwt_scriptsLoaded){m.__gwt_scriptsLoaded={}}function B(){var b=false;try{var c=m.location.search;return (c.indexOf(U)!=-1||(c.indexOf(V)!=-1||m.external&&m.external.gwtOnLoad))&&c.indexOf(W)==-1}catch(a){}B=function(){return b};return b} function genex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='3F4ADBED36D589545A9300A1EA686D36',Rb='73F4B6D6D466BAD6850A60128DF5B80D',Wb=':',pb='::',dc='<script defer="defer">genex.onInjectionDone(\'genex\')<\/script>',hb='<script id="',sb='=',$='?',Sb='BA18AC23ACC5016C5D0799E864BBDFFE',ub='Bad handler "',Tb='C7B18436BA03373FB13ED589C2CCF417',cc='DOMContentLoaded',Ub='E1A9A95677AFC620CAD5759B7ACC3E67',Vb='FF175D5583BDD5ACF40C7F0AFF9A374B',jb='SCRIPT',gb='__gwt_marker_genex',kb='base',cb='baseUrl',T='begin',S='bootstrap',bb='clear.cache.gif',rb='content',Y='end',Kb='gecko',Lb='gecko1_8',Q='genex',Yb='genex.css',eb='genex.nocache.js',ob='genex::',U='gwt.codesvr=',V='gwt.hosted=',W='gwt.hybrid',wb='gwt:onLoadErrorFn',tb='gwt:onPropertyErrorFn',qb='gwt:property',bc='head',Ob='hosted.html?genex',ac='href',Jb='ie6',Ib='ie8',Hb='ie9',yb='iframe',ab='img',zb="javascript:''",Zb='link',Nb='loadExternalRefs',mb='meta',Bb='moduleRequested',X='moduleStartup',Gb='msie',nb='name',Db='opera',Ab='position:absolute;width:0;height:0;border:none',$b='rel',Fb='safari',db='script',Pb='selectingPermutation',R='startup',_b='stylesheet',fb='undefined',Mb='unknown',Cb='user.agent',Eb='webkit';var m=window,n=document,o=m.__gwtStatsEvent?function(a){return m.__gwtStatsEvent(a)}:null,p=m.__gwtStatsSessionId?m.__gwtStatsSessionId:null,q,r,s,t=P,u={},v=[],w=[],x=[],y=0,z,A;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:T});if(!m.__gwt_stylesLoaded){m.__gwt_stylesLoaded={}}if(!m.__gwt_scriptsLoaded){m.__gwt_scriptsLoaded={}}function B(){var b=false;try{var c=m.location.search;return (c.indexOf(U)!=-1||(c.indexOf(V)!=-1||m.external&&m.external.gwtOnLoad))&&c.indexOf(W)==-1}catch(a){}B=function(){return b};return b}
function C(){if(q&&r){var b=n.getElementById(Q);var c=b.contentWindow;if(B()){c.__gwt_getProperty=function(a){return H(a)}}genex=null;c.gwtOnLoad(z,Q,t,y);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Y})}} function C(){if(q&&r){var b=n.getElementById(Q);var c=b.contentWindow;if(B()){c.__gwt_getProperty=function(a){return H(a)}}genex=null;c.gwtOnLoad(z,Q,t,y);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Y})}}
function D(){function e(a){var b=a.lastIndexOf(Z);if(b==-1){b=a.length}var c=a.indexOf($);if(c==-1){c=a.length}var d=a.lastIndexOf(_,Math.min(c,b));return d>=0?a.substring(0,d+1):P} function D(){function e(a){var b=a.lastIndexOf(Z);if(b==-1){b=a.length}var c=a.indexOf($);if(c==-1){c=a.length}var d=a.lastIndexOf(_,Math.min(c,b));return d>=0?a.substring(0,d+1):P}
function f(a){if(a.match(/^\w+:\/\//)){}else{var b=n.createElement(ab);b.src=a+bb;a=e(b.src)}return a} function f(a){if(a.match(/^\w+:\/\//)){}else{var b=n.createElement(ab);b.src=a+bb;a=e(b.src)}return a}
...@@ -13,6 +13,6 @@ function F(a){var b=u[a];return b==null?null:b} ...@@ -13,6 +13,6 @@ function F(a){var b=u[a];return b==null?null:b}
function G(a,b){var c=x;for(var d=0,e=a.length-1;d<e;++d){c=c[a[d]]||(c[a[d]]=[])}c[a[e]]=b} function G(a,b){var c=x;for(var d=0,e=a.length-1;d<e;++d){c=c[a[d]]||(c[a[d]]=[])}c[a[e]]=b}
function H(a){var b=w[a](),c=v[a];if(b in c){return b}var d=[];for(var e in c){d[c[e]]=e}if(A){A(a,d,b)}throw null} function H(a){var b=w[a](),c=v[a];if(b in c){return b}var d=[];for(var e in c){d[c[e]]=e}if(A){A(a,d,b)}throw null}
var I;function J(){if(!I){I=true;var a=n.createElement(yb);a.src=zb;a.id=Q;a.style.cssText=Ab;a.tabIndex=-1;n.body.appendChild(a);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Bb});a.contentWindow.location.replace(t+L)}} var I;function J(){if(!I){I=true;var a=n.createElement(yb);a.src=zb;a.id=Q;a.style.cssText=Ab;a.tabIndex=-1;n.body.appendChild(a);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Bb});a.contentWindow.location.replace(t+L)}}
w[Cb]=function(){var b=navigator.userAgent.toLowerCase();var c=function(a){return parseInt(a[1])*1000+parseInt(a[2])};if(function(){return b.indexOf(Db)!=-1}())return Db;if(function(){return b.indexOf(Eb)!=-1}())return Fb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=9}())return Hb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=8}())return Ib;if(function(){var a=/msie ([0-9]+)\.([0-9]+)/.exec(b);if(a&&a.length==3)return c(a)>=6000}())return Jb;if(function(){return b.indexOf(Kb)!=-1}())return Lb;return Mb};v[Cb]={gecko1_8:0,ie6:1,ie8:2,ie9:3,opera:4,safari:5};genex.onScriptLoad=function(){if(I){r=true;C()}};genex.onInjectionDone=function(){q=true;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:Y});C()};E();D();var K;var L;if(B()){if(m.external&&(m.external.initModule&&m.external.initModule(Q))){m.location.reload();return}L=Ob;K=P}o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Pb});if(!B()){try{G([Fb],Qb);G([Lb],Rb);G([Hb],Sb);G([Jb],Tb);G([Db],Ub);G([Ib],Vb);K=x[H(Cb)];var M=K.indexOf(Wb);if(M!=-1){y=Number(K.substring(M+1));K=K.substring(0,M)}L=K+Xb}catch(a){return}}var N;function O(){if(!s){s=true;if(!__gwt_stylesLoaded[Yb]){var a=n.createElement(Zb);__gwt_stylesLoaded[Yb]=a;a.setAttribute($b,_b);a.setAttribute(ac,t+Yb);n.getElementsByTagName(bc)[0].appendChild(a)}C();if(n.removeEventListener){n.removeEventListener(cc,O,false)}if(N){clearInterval(N)}}} w[Cb]=function(){var b=navigator.userAgent.toLowerCase();var c=function(a){return parseInt(a[1])*1000+parseInt(a[2])};if(function(){return b.indexOf(Db)!=-1}())return Db;if(function(){return b.indexOf(Eb)!=-1}())return Fb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=9}())return Hb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=8}())return Ib;if(function(){var a=/msie ([0-9]+)\.([0-9]+)/.exec(b);if(a&&a.length==3)return c(a)>=6000}())return Jb;if(function(){return b.indexOf(Kb)!=-1}())return Lb;return Mb};v[Cb]={gecko1_8:0,ie6:1,ie8:2,ie9:3,opera:4,safari:5};genex.onScriptLoad=function(){if(I){r=true;C()}};genex.onInjectionDone=function(){q=true;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:Y});C()};E();D();var K;var L;if(B()){if(m.external&&(m.external.initModule&&m.external.initModule(Q))){m.location.reload();return}L=Ob;K=P}o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Pb});if(!B()){try{G([Hb],Qb);G([Fb],Rb);G([Ib],Sb);G([Lb],Tb);G([Db],Ub);G([Jb],Vb);K=x[H(Cb)];var M=K.indexOf(Wb);if(M!=-1){y=Number(K.substring(M+1));K=K.substring(0,M)}L=K+Xb}catch(a){return}}var N;function O(){if(!s){s=true;if(!__gwt_stylesLoaded[Yb]){var a=n.createElement(Zb);__gwt_stylesLoaded[Yb]=a;a.setAttribute($b,_b);a.setAttribute(ac,t+Yb);n.getElementsByTagName(bc)[0].appendChild(a)}C();if(n.removeEventListener){n.removeEventListener(cc,O,false)}if(N){clearInterval(N)}}}
if(n.addEventListener){n.addEventListener(cc,function(){J();O()},false)}var N=setInterval(function(){if(/loaded|complete/.test(n.readyState)){J();O()}},50);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Y});o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:T});n.write(dc)} if(n.addEventListener){n.addEventListener(cc,function(){J();O()},false)}var N=setInterval(function(){if(/loaded|complete/.test(n.readyState)){J();O()}},50);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Y});o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:T});n.write(dc)}
genex(); genex();
\ 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'),
......
...@@ -116,6 +116,10 @@ def create_thread(request, course_id, commentable_id): ...@@ -116,6 +116,10 @@ def create_thread(request, course_id, commentable_id):
thread.save() thread.save()
#patch for backward compatibility to comments service
if not 'pinned' in thread.attributes:
thread['pinned'] = False
if post.get('auto_subscribe', 'false').lower() == 'true': if post.get('auto_subscribe', 'false').lower() == 'true':
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
user.follow(thread) user.follow(thread)
...@@ -289,6 +293,21 @@ def undo_vote_for_thread(request, course_id, thread_id): ...@@ -289,6 +293,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'])
...@@ -98,6 +99,11 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG ...@@ -98,6 +99,11 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
thread['group_name'] = "" thread['group_name'] = ""
thread['group_string'] = "This post visible to everyone." thread['group_string'] = "This post visible to everyone."
#patch for backward compatibility to comments service
if not 'pinned' in thread:
thread['pinned'] = False
query_params['page'] = page query_params['page'] = page
query_params['num_pages'] = num_pages query_params['num_pages'] = num_pages
...@@ -210,6 +216,9 @@ def forum_form_discussion(request, course_id): ...@@ -210,6 +216,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,
...@@ -241,6 +250,11 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -241,6 +250,11 @@ def single_thread(request, course_id, discussion_id, thread_id):
try: try:
thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id)
#patch for backward compatibility with comments service
if not 'pinned' in thread.attributes:
thread['pinned'] = False
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
log.error("Error loading single thread.") log.error("Error loading single thread.")
raise Http404 raise Http404
...@@ -281,6 +295,10 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -281,6 +295,10 @@ def single_thread(request, course_id, discussion_id, thread_id):
if thread.get('group_id') and not thread.get('group_name'): if thread.get('group_id') and not thread.get('group_name'):
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
#patch for backward compatibility with comments service
if not "pinned" in thread:
thread["pinned"] = False
threads = [utils.safe_content(thread) for thread in threads] threads = [utils.safe_content(thread) for thread in threads]
#recent_active_threads = cc.search_recent_active_threads( #recent_active_threads = cc.search_recent_active_threads(
......
...@@ -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):
......
from utils import * from .utils import *
class Model(object): class Model(object):
......
...@@ -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,18 @@ ...@@ -45,6 +45,18 @@
</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>)
......
...@@ -6,9 +6,18 @@ ...@@ -6,9 +6,18 @@
<link type="text/html" rel="alternate" href="http://blog.edx.org/"/> <link type="text/html" rel="alternate" href="http://blog.edx.org/"/>
##<link type="application/atom+xml" rel="self" href="https://github.com/blog.atom"/> ##<link type="application/atom+xml" rel="self" href="https://github.com/blog.atom"/>
<title>EdX Blog</title> <title>EdX Blog</title>
<updated>2013-02-20T14:00:12-07:00</updated> <updated>2013-03-14T14:00:12-07:00</updated>
<entry> <entry>
<id>tag:www.edx.org,2012:Post/13</id> <id>tag:www.edx.org,2013:Post/15</id>
<published>2013-03-14T10:00:00-07:00</published>
<updated>2013-03-14T10:00:00-07:00</updated>
<link type="text/html" rel="alternate" href="/courses/MITx/2.01x/2013_Spring/about"/>
<title>New mechanical engineering course open for enrollment</title>
<content type="html">&lt;img src=&quot;${static.url('images/press/releases/201x_240x180.jpg')}&quot; /&gt;
&lt;p&gt;&lt;/p&gt;</content>
</entry>
<entry>
<id>tag:www.edx.org,2013:Post/14</id>
<published>2013-02-20T10:00:00-07:00</published> <published>2013-02-20T10:00:00-07:00</published>
<updated>2013-02-20T10:00:00-07:00</updated> <updated>2013-02-20T10:00:00-07:00</updated>
<link type="text/html" rel="alternate" href="${reverse('press/edx-expands-internationally')}"/> <link type="text/html" rel="alternate" href="${reverse('press/edx-expands-internationally')}"/>
...@@ -17,7 +26,7 @@ ...@@ -17,7 +26,7 @@
&lt;p&gt;&lt;/p&gt;</content> &lt;p&gt;&lt;/p&gt;</content>
</entry> </entry>
<entry> <entry>
<id>tag:www.edx.org,2012:Post/13</id> <id>tag:www.edx.org,2013:Post/14</id>
<published>2013-01-30T10:00:00-07:00</published> <published>2013-01-30T10:00:00-07:00</published>
<updated>2013-01-30T10:00:00-07:00</updated> <updated>2013-01-30T10:00:00-07:00</updated>
<link type="text/html" rel="alternate" href="${reverse('press/eric-lander-secret-of-life')}"/> <link type="text/html" rel="alternate" href="${reverse('press/eric-lander-secret-of-life')}"/>
...@@ -26,7 +35,7 @@ ...@@ -26,7 +35,7 @@
&lt;p&gt;&lt;/p&gt;</content> &lt;p&gt;&lt;/p&gt;</content>
</entry> </entry>
<entry> <entry>
<id>tag:www.edx.org,2012:Post/11</id> <id>tag:www.edx.org,2013:Post/12</id>
<published>2013-01-22T10:00:00-07:00</published> <published>2013-01-22T10:00:00-07:00</published>
<updated>2013-01-22T10:00:00-07:00</updated> <updated>2013-01-22T10:00:00-07:00</updated>
<link type="text/html" rel="alternate" href="${reverse('press/lewin-course-announcement')}"/> <link type="text/html" rel="alternate" href="${reverse('press/lewin-course-announcement')}"/>
...@@ -35,7 +44,7 @@ ...@@ -35,7 +44,7 @@
&lt;p&gt;&lt;/p&gt;</content> &lt;p&gt;&lt;/p&gt;</content>
</entry> </entry>
<entry> <entry>
<id>tag:www.edx.org,2012:Post/12</id> <id>tag:www.edx.org,2013:Post/11</id>
<published>2013-01-29T10:00:00-07:00</published> <published>2013-01-29T10:00:00-07:00</published>
<updated>2013-01-29T10:00:00-07:00</updated> <updated>2013-01-29T10:00:00-07:00</updated>
<link type="text/html" rel="alternate" href="${reverse('press/bostonx-announcement')}"/> <link type="text/html" rel="alternate" href="${reverse('press/bostonx-announcement')}"/>
......
...@@ -48,6 +48,7 @@ ...@@ -48,6 +48,7 @@
<li>A great working experience where everyone cares and wants to change the world (no, we’re not kidding)</li> <li>A great working experience where everyone cares and wants to change the world (no, we’re not kidding)</li>
</ul> </ul>
<p>While we appreciate every applicant&rsquo;s interest, only those under consideration will be contacted. We regret that phone calls will not be accepted. Equal opportunity employer.</p> <p>While we appreciate every applicant&rsquo;s interest, only those under consideration will be contacted. We regret that phone calls will not be accepted. Equal opportunity employer.</p>
<p>All positions are located in our Cambridge offices.</p>
</div> </div>
</article> </article>
......
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