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/
.idea/
.redcar/
chromedriver.log
/nbproject
ghostdriver.log
......@@ -310,11 +310,26 @@ class CapaModule(XModule):
is_survey_question = (self.max_attempts == 0)
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),
# 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
# then do NOT show the reset button
if (self.closed() and not is_survey_question) or needs_reset:
# then do NOT show the save button
elif (self.closed() and not is_survey_question) or needs_reset:
return False
else:
return True
......@@ -403,7 +418,7 @@ class CapaModule(XModule):
# if we want to show a check button, and False otherwise
# This works because non-empty strings evaluate to True
if self.should_show_check_button():
check_button = self.check_button_name()
check_button = self.check_button_name()
else:
check_button = False
......@@ -556,7 +571,7 @@ class CapaModule(XModule):
else:
answers = self.lcp.get_question_answers()
# answers (eg <solution>) may have embedded images
# answers (eg <solution>) may have embedded images
# but be careful, some problems are using non-string answer dicts
new_answers = dict()
for answer_id in answers:
......@@ -606,7 +621,7 @@ class CapaModule(XModule):
to 'input_1' in the returned dict)
'''
answers = dict()
for key in get:
# e.g. input_resistor_1 ==> resistor_1
_, _, name = key.partition('_')
......@@ -729,7 +744,7 @@ class CapaModule(XModule):
event_info['answers'] = answers
# 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'
self.system.track_function('save_problem_fail', event_info)
return {'success': False,
......@@ -747,7 +762,7 @@ class CapaModule(XModule):
self.system.track_function('save_problem_success', event_info)
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."
return {'success': True,
'msg': msg}
......@@ -784,7 +799,7 @@ class CapaModule(XModule):
# reset random number generator seed (note the self.lcp.get_state()
# in next line)
self.lcp.seed = None
self.lcp = LoncapaProblem(self.definition['data'],
self.location.html_id(), self.lcp.get_state(),
......@@ -793,7 +808,7 @@ class CapaModule(XModule):
event_info['new_state'] = self.lcp.get_state()
self.system.track_function('reset_problem', event_info)
return { 'success': True,
return {'success': True,
'html': self.get_problem_html(encapsulate=False)}
......@@ -821,13 +836,13 @@ class CapaDescriptor(RawDescriptor):
def get_context(self):
_context = RawDescriptor.get_context(self)
_context.update({'markdown': self.metadata.get('markdown', ''),
'enable_markdown' : 'markdown' in self.metadata})
'enable_markdown': 'markdown' in self.metadata})
return _context
@property
def editable_metadata_fields(self):
"""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']]
return subset
......
......@@ -87,7 +87,7 @@ class FolditModule(XModule):
from foldit.models import Score
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
......
......@@ -79,6 +79,17 @@ if Backbone?
@getContent(id).updateInfo(info)
$.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
urlMappers:
'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id)
......@@ -91,6 +102,8 @@ if Backbone?
'delete' : -> DiscussionUtil.urlFor('delete_thread', @id)
'follow' : -> DiscussionUtil.urlFor('follow_thread', @id)
'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id)
'pinThread' : -> DiscussionUtil.urlFor("pin_thread", @id)
'unPinThread' : -> DiscussionUtil.urlFor("un_pin_thread", @id)
initialize: ->
@set('thread', @)
......
......@@ -58,10 +58,31 @@ if Backbone?
@current_page = response.page
sortByDate: (thread) ->
thread.get("created_at")
#
#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")
sortByDateRecentFirst: (thread) ->
-(new Date(thread.get("created_at")).getTime())
#
#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())
#return String.fromCharCode.apply(String,
# _.map(thread.get("created_at").split(""),
# ((c) -> return 0xffff - c.charChodeAt()))
......
......@@ -50,6 +50,8 @@ class @DiscussionUtil
delete_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/delete"
upvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/upvote"
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"
follow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/follow"
unfollow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unfollow"
......
......@@ -3,6 +3,7 @@ if Backbone?
events:
"click .discussion-vote": "toggleVote"
"click .admin-pin": "togglePin"
"click .action-follow": "toggleFollowing"
"click .action-edit": "edit"
"click .action-delete": "delete"
......@@ -24,6 +25,7 @@ if Backbone?
@delegateEvents()
@renderDogear()
@renderVoted()
@renderPinned()
@renderAttrs()
@$("span.timeago").timeago()
@convertMath()
......@@ -41,8 +43,20 @@ if Backbone?
else
@$("[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: =>
@renderVoted()
@renderPinned()
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"])
convertMath: ->
......@@ -99,6 +113,36 @@ if Backbone?
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) ->
$elem = $(event.target)
url = @model.urlFor('close')
......@@ -137,3 +181,5 @@ if Backbone?
if @model.get('username')?
params = $.extend(params, user:{username: @model.username, user_url: @model.user_url})
Mustache.render(@template, params)
\ No newline at end of file
......@@ -57,10 +57,15 @@ pre, #dna-strand {
background: white;
}
.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 {
background: url(images/vborder.png) repeat-y -31px 0px;
}
.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 {
width: 10px;
......@@ -82,12 +87,20 @@ pre, #dna-strand {
zoom: 1;
}
.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 {
background: url(images/circles.png) no-repeat -28px 0px;
-background: url(images/circles_ie6.png) no-repeat -28px 0px;
}
.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 {
background: url(images/circles.png) no-repeat -8px -36px;
-background: url(images/circles_ie6.png) no-repeat -8px -36px;
}
* html .gwt-DialogBox .dialogTopLeftInner {
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 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}
......@@ -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 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)}}
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)}
genex();
\ No newline at end of file
......@@ -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\-]+)/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\-]+)/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\-]+)/unfollow$', 'unfollow_thread', name='unfollow_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):
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':
user = cc.User.from_django_user(request.user)
user.follow(thread)
......@@ -289,6 +293,21 @@ def undo_vote_for_thread(request, course_id, thread_id):
user.unvote(thread)
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
@login_required
......
......@@ -91,12 +91,18 @@ 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
for thread in threads:
if thread.get('group_id'):
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'])
else:
thread['group_name'] = ""
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['num_pages'] = num_pages
......@@ -210,6 +216,9 @@ def forum_form_discussion(request, course_id):
user_cohort_id = get_cohort_id(request.user, course_id)
context = {
'csrf': csrf(request)['csrf_token'],
'course': course,
......@@ -241,6 +250,11 @@ def single_thread(request, course_id, discussion_id, thread_id):
try:
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:
log.error("Error loading single thread.")
raise Http404
......@@ -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'):
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]
#recent_active_threads = cc.search_recent_active_threads(
......
......@@ -90,6 +90,8 @@ VIEW_PERMISSIONS = {
'undo_vote_for_comment': [['unvote', 'is_open']],
'vote_for_thread' : [['vote', 'is_open']],
'undo_vote_for_thread': [['unvote', 'is_open']],
'pin_thread': ['create_comment'],
'un_pin_thread': ['create_comment'],
'follow_thread' : ['follow_thread'],
'follow_commentable': ['follow_commentable'],
'follow_user' : ['follow_user'],
......
......@@ -406,7 +406,7 @@ 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'
'read', 'group_id', 'group_name', 'group_string', 'pinned'
]
if (content.get('anonymous') is False) and (content.get('anonymous_to_peers') is False):
......
from utils import *
from .utils import *
class Model(object):
......
......@@ -11,12 +11,12 @@ class Thread(models.Model):
'closed', 'tags', 'votes', 'commentable_id', 'username', 'user_id',
'created_at', 'updated_at', 'comments_count', 'unread_comments_count',
'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 = [
'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
......@@ -79,3 +79,23 @@ class Thread(models.Model):
response = perform_request('get', url, request_params)
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
......@@ -2442,4 +2442,39 @@ body.discussion {
color:#000;
font-style: italic;
background-color:#fff;
}
\ No newline at end of file
}
.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 @@
</header>
<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) { %>'}
<div class="post-context">
(this post is about <a href="${'<%- courseware_url%>'}">${'<%- courseware_title %>'}</a>)
......
......@@ -6,9 +6,18 @@
<link type="text/html" rel="alternate" href="http://blog.edx.org/"/>
##<link type="application/atom+xml" rel="self" href="https://github.com/blog.atom"/>
<title>EdX Blog</title>
<updated>2013-02-20T14:00:12-07:00</updated>
<updated>2013-03-14T14:00:12-07:00</updated>
<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>
<updated>2013-02-20T10:00:00-07:00</updated>
<link type="text/html" rel="alternate" href="${reverse('press/edx-expands-internationally')}"/>
......@@ -17,7 +26,7 @@
&lt;p&gt;&lt;/p&gt;</content>
</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>
<updated>2013-01-30T10:00:00-07:00</updated>
<link type="text/html" rel="alternate" href="${reverse('press/eric-lander-secret-of-life')}"/>
......@@ -26,7 +35,7 @@
&lt;p&gt;&lt;/p&gt;</content>
</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>
<updated>2013-01-22T10:00:00-07:00</updated>
<link type="text/html" rel="alternate" href="${reverse('press/lewin-course-announcement')}"/>
......@@ -35,7 +44,7 @@
&lt;p&gt;&lt;/p&gt;</content>
</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>
<updated>2013-01-29T10:00:00-07:00</updated>
<link type="text/html" rel="alternate" href="${reverse('press/bostonx-announcement')}"/>
......
......@@ -48,11 +48,12 @@
<li>A great working experience where everyone cares and wants to change the world (no, we’re not kidding)</li>
</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>All positions are located in our Cambridge offices.</p>
</div>
</article>
<!-- Template
<!-- Template
<article id="SOME-JOB" class="job">
<div class="inner-wrapper">
<h3><strong>TITLE</strong></h3>
......
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