Commit 029d8c80 by Calen Pennington

Merge remote-tracking branch 'origin/master' into feature/alex/poll-merged

Conflicts:
	common/lib/xmodule/xmodule/capa_module.py
	common/lib/xmodule/xmodule/tests/test_capa_module.py
parents 116ecf2a 7eb4970f
...@@ -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
...@@ -288,11 +288,26 @@ class CapaModule(CapaFields, XModule): ...@@ -288,11 +288,26 @@ class CapaModule(CapaFields, 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
...@@ -722,7 +737,7 @@ class CapaModule(CapaFields, XModule): ...@@ -722,7 +737,7 @@ class CapaModule(CapaFields, 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,
...@@ -742,7 +757,7 @@ class CapaModule(CapaFields, XModule): ...@@ -742,7 +757,7 @@ class CapaModule(CapaFields, 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}
...@@ -790,7 +805,7 @@ class CapaModule(CapaFields, XModule): ...@@ -790,7 +805,7 @@ class CapaModule(CapaFields, 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)}
......
...@@ -351,7 +351,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -351,7 +351,7 @@ class CapaModuleTest(unittest.TestCase):
valid_get_dict = self._querydict_from_dict({'input_2[]': ['test1', 'test2']}) valid_get_dict = self._querydict_from_dict({'input_2[]': ['test1', 'test2']})
result = CapaModule.make_dict_of_responses(valid_get_dict) result = CapaModule.make_dict_of_responses(valid_get_dict)
self.assertTrue('2' in result) self.assertTrue('2' in result)
self.assertEqual(['test1','test2'], result['2']) self.assertEqual(['test1', 'test2'], result['2'])
# If we use [] at the end of a key name, we should always # If we use [] at the end of a key name, we should always
# get a list, even if there's just one value # get a list, even if there's just one value
...@@ -369,7 +369,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -369,7 +369,7 @@ class CapaModuleTest(unittest.TestCase):
# One of the values would overwrite the other, so detect this # One of the values would overwrite the other, so detect this
# and raise an exception # and raise an exception
invalid_get_dict = self._querydict_from_dict({'input_1[]': 'test 1', invalid_get_dict = self._querydict_from_dict({'input_1[]': 'test 1',
'input_1': 'test 2' }) 'input_1': 'test 2'})
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
result = CapaModule.make_dict_of_responses(invalid_get_dict) result = CapaModule.make_dict_of_responses(invalid_get_dict)
...@@ -407,7 +407,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -407,7 +407,7 @@ class CapaModuleTest(unittest.TestCase):
mock_html.return_value = "Test HTML" mock_html.return_value = "Test HTML"
# Check the problem # Check the problem
get_request_dict = { CapaFactory.input_key(): '3.14' } get_request_dict = { CapaFactory.input_key(): '3.14'}
result = module.check_problem(get_request_dict) result = module.check_problem(get_request_dict)
# Expect that the problem is marked correct # Expect that the problem is marked correct
...@@ -428,7 +428,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -428,7 +428,7 @@ class CapaModuleTest(unittest.TestCase):
mock_is_correct.return_value = False mock_is_correct.return_value = False
# Check the problem # Check the problem
get_request_dict = { CapaFactory.input_key(): '0' } get_request_dict = { CapaFactory.input_key(): '0'}
result = module.check_problem(get_request_dict) result = module.check_problem(get_request_dict)
# Expect that the problem is marked correct # Expect that the problem is marked correct
...@@ -446,7 +446,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -446,7 +446,7 @@ class CapaModuleTest(unittest.TestCase):
with patch('xmodule.capa_module.CapaModule.closed') as mock_closed: with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
mock_closed.return_value = True mock_closed.return_value = True
with self.assertRaises(xmodule.exceptions.NotFoundError): with self.assertRaises(xmodule.exceptions.NotFoundError):
get_request_dict = { CapaFactory.input_key(): '3.14' } get_request_dict = { CapaFactory.input_key(): '3.14'}
module.check_problem(get_request_dict) module.check_problem(get_request_dict)
# Expect that number of attempts NOT incremented # Expect that number of attempts NOT incremented
...@@ -492,7 +492,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -492,7 +492,7 @@ class CapaModuleTest(unittest.TestCase):
mock_is_queued.return_value = True mock_is_queued.return_value = True
mock_get_queuetime.return_value = datetime.datetime.now() mock_get_queuetime.return_value = datetime.datetime.now()
get_request_dict = { CapaFactory.input_key(): '3.14' } get_request_dict = { CapaFactory.input_key(): '3.14'}
result = module.check_problem(get_request_dict) result = module.check_problem(get_request_dict)
# Expect an AJAX alert message in 'success' # Expect an AJAX alert message in 'success'
...@@ -509,7 +509,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -509,7 +509,7 @@ class CapaModuleTest(unittest.TestCase):
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') mock_grade.side_effect = capa.responsetypes.StudentInputError('test error')
get_request_dict = { CapaFactory.input_key(): '3.14' } get_request_dict = { CapaFactory.input_key(): '3.14'}
result = module.check_problem(get_request_dict) result = module.check_problem(get_request_dict)
# Expect an AJAX alert message in 'success' # Expect an AJAX alert message in 'success'
...@@ -573,11 +573,11 @@ class CapaModuleTest(unittest.TestCase): ...@@ -573,11 +573,11 @@ class CapaModuleTest(unittest.TestCase):
module = CapaFactory.create(done=False) module = CapaFactory.create(done=False)
# Save the problem # Save the problem
get_request_dict = { CapaFactory.input_key(): '3.14' } get_request_dict = { CapaFactory.input_key(): '3.14'}
result = module.save_problem(get_request_dict) result = module.save_problem(get_request_dict)
# Expect that answers are saved to the problem # Expect that answers are saved to the problem
expected_answers = { CapaFactory.answer_key(): '3.14' } expected_answers = { CapaFactory.answer_key(): '3.14'}
self.assertEqual(module.lcp.student_answers, expected_answers) self.assertEqual(module.lcp.student_answers, expected_answers)
# Expect that the result is success # Expect that the result is success
...@@ -592,7 +592,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -592,7 +592,7 @@ class CapaModuleTest(unittest.TestCase):
mock_closed.return_value = True mock_closed.return_value = True
# Try to save the problem # Try to save the problem
get_request_dict = { CapaFactory.input_key(): '3.14' } get_request_dict = { CapaFactory.input_key(): '3.14'}
result = module.save_problem(get_request_dict) result = module.save_problem(get_request_dict)
# Expect that the result is failure # Expect that the result is failure
...@@ -603,7 +603,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -603,7 +603,7 @@ class CapaModuleTest(unittest.TestCase):
module = CapaFactory.create(rerandomize='always', done=True) module = CapaFactory.create(rerandomize='always', done=True)
# Try to save # Try to save
get_request_dict = { CapaFactory.input_key(): '3.14' } get_request_dict = { CapaFactory.input_key(): '3.14'}
result = module.save_problem(get_request_dict) result = module.save_problem(get_request_dict)
# Expect that we cannot save # Expect that we cannot save
...@@ -614,7 +614,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -614,7 +614,7 @@ class CapaModuleTest(unittest.TestCase):
module = CapaFactory.create(rerandomize='never', done=True) module = CapaFactory.create(rerandomize='never', done=True)
# Try to save # Try to save
get_request_dict = { CapaFactory.input_key(): '3.14' } get_request_dict = { CapaFactory.input_key(): '3.14'}
result = module.save_problem(get_request_dict) result = module.save_problem(get_request_dict)
# Expect that we succeed # Expect that we succeed
...@@ -626,7 +626,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -626,7 +626,7 @@ class CapaModuleTest(unittest.TestCase):
# Just in case, we also check what happens if we have # Just in case, we also check what happens if we have
# more attempts than allowed. # more attempts than allowed.
attempts = random.randint(1, 10) attempts = random.randint(1, 10)
module = CapaFactory.create(attempts=attempts-1, max_attempts=attempts) module = CapaFactory.create(attempts=attempts -1, max_attempts=attempts)
self.assertEqual(module.check_button_name(), "Final Check") self.assertEqual(module.check_button_name(), "Final Check")
module = CapaFactory.create(attempts=attempts, max_attempts=attempts) module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
...@@ -636,14 +636,14 @@ class CapaModuleTest(unittest.TestCase): ...@@ -636,14 +636,14 @@ class CapaModuleTest(unittest.TestCase):
self.assertEqual(module.check_button_name(), "Final Check") self.assertEqual(module.check_button_name(), "Final Check")
# Otherwise, button name is "Check" # Otherwise, button name is "Check"
module = CapaFactory.create(attempts=attempts-2, max_attempts=attempts) module = CapaFactory.create(attempts=attempts -2, max_attempts=attempts)
self.assertEqual(module.check_button_name(), "Check") self.assertEqual(module.check_button_name(), "Check")
module = CapaFactory.create(attempts=attempts-3, max_attempts=attempts) module = CapaFactory.create(attempts=attempts -3, max_attempts=attempts)
self.assertEqual(module.check_button_name(), "Check") self.assertEqual(module.check_button_name(), "Check")
# If no limit on attempts, then always show "Check" # If no limit on attempts, then always show "Check"
module = CapaFactory.create(attempts=attempts-3) module = CapaFactory.create(attempts=attempts -3)
self.assertEqual(module.check_button_name(), "Check") self.assertEqual(module.check_button_name(), "Check")
module = CapaFactory.create(attempts=0) module = CapaFactory.create(attempts=0)
...@@ -651,7 +651,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -651,7 +651,7 @@ class CapaModuleTest(unittest.TestCase):
def test_should_show_check_button(self): def test_should_show_check_button(self):
attempts = random.randint(1,10) attempts = random.randint(1, 10)
# If we're after the deadline, do NOT show check button # If we're after the deadline, do NOT show check button
module = CapaFactory.create(due=self.yesterday_str) module = CapaFactory.create(due=self.yesterday_str)
...@@ -685,7 +685,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -685,7 +685,7 @@ class CapaModuleTest(unittest.TestCase):
def test_should_show_reset_button(self): def test_should_show_reset_button(self):
attempts = random.randint(1,10) attempts = random.randint(1, 10)
# If we're after the deadline, do NOT show the reset button # If we're after the deadline, do NOT show the reset button
module = CapaFactory.create(due=self.yesterday_str, done=True) module = CapaFactory.create(due=self.yesterday_str, done=True)
...@@ -716,7 +716,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -716,7 +716,7 @@ class CapaModuleTest(unittest.TestCase):
def test_should_show_save_button(self): def test_should_show_save_button(self):
attempts = random.randint(1,10) attempts = random.randint(1, 10)
# If we're after the deadline, do NOT show the save button # If we're after the deadline, do NOT show the save button
module = CapaFactory.create(due=self.yesterday_str, done=True) module = CapaFactory.create(due=self.yesterday_str, done=True)
...@@ -730,6 +730,17 @@ class CapaModuleTest(unittest.TestCase): ...@@ -730,6 +730,17 @@ class CapaModuleTest(unittest.TestCase):
module = CapaFactory.create(rerandomize="always", done=True) module = CapaFactory.create(rerandomize="always", done=True)
self.assertFalse(module.should_show_save_button()) self.assertFalse(module.should_show_save_button())
# If the user has unlimited attempts and we are not randomizing,
# then do NOT show a save button
# because they can keep using "Check"
module = CapaFactory.create(max_attempts=None, rerandomize="never")
module.lcp.done = False
self.assertFalse(module.should_show_save_button())
module = CapaFactory.create(max_attempts=None, rerandomize="never")
module.lcp.done = True
self.assertFalse(module.should_show_save_button())
# Otherwise, DO show the save button # Otherwise, DO show the save button
module = CapaFactory.create(done=False) module = CapaFactory.create(done=False)
self.assertTrue(module.should_show_save_button()) self.assertTrue(module.should_show_save_button())
...@@ -778,9 +789,9 @@ class CapaModuleTest(unittest.TestCase): ...@@ -778,9 +789,9 @@ class CapaModuleTest(unittest.TestCase):
# We've tested the show/hide button logic in other tests, # We've tested the show/hide button logic in other tests,
# so here we hard-wire the values # so here we hard-wire the values
show_check_button = bool(random.randint(0,1) % 2) show_check_button = bool(random.randint(0, 1) % 2)
show_reset_button = bool(random.randint(0,1) % 2) show_reset_button = bool(random.randint(0, 1) % 2)
show_save_button = bool(random.randint(0,1) % 2) show_save_button = bool(random.randint(0, 1) % 2)
module.should_show_check_button = Mock(return_value=show_check_button) module.should_show_check_button = Mock(return_value=show_check_button)
module.should_show_reset_button = Mock(return_value=show_reset_button) module.should_show_reset_button = Mock(return_value=show_reset_button)
...@@ -803,7 +814,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -803,7 +814,7 @@ class CapaModuleTest(unittest.TestCase):
self.assertEqual(html, "<div>Test Template HTML</div>") self.assertEqual(html, "<div>Test Template HTML</div>")
# Check the rendering context # Check the rendering context
render_args,_ = module.system.render_template.call_args render_args, _ = module.system.render_template.call_args
self.assertEqual(len(render_args), 2) self.assertEqual(len(render_args), 2)
template_name = render_args[0] template_name = render_args[0]
...@@ -844,7 +855,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -844,7 +855,7 @@ class CapaModuleTest(unittest.TestCase):
html = module.get_problem_html() html = module.get_problem_html()
# Check the rendering context # Check the rendering context
render_args,_ = module.system.render_template.call_args render_args, _ = module.system.render_template.call_args
context = render_args[1] context = render_args[1]
self.assertTrue("error" in context['problem']['html']) self.assertTrue("error" in context['problem']['html'])
......
...@@ -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,10 +58,31 @@ if Backbone? ...@@ -58,10 +58,31 @@ if Backbone?
@current_page = response.page @current_page = response.page
sortByDate: (thread) -> 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) -> 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, #return String.fromCharCode.apply(String,
# _.map(thread.get("created_at").split(""), # _.map(thread.get("created_at").split(""),
# ((c) -> return 0xffff - c.charChodeAt())) # ((c) -> return 0xffff - c.charChodeAt()))
......
...@@ -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
...@@ -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'),
......
...@@ -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'],
......
...@@ -408,7 +408,7 @@ def safe_content(content): ...@@ -408,7 +408,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
...@@ -2442,4 +2442,39 @@ body.discussion { ...@@ -2442,4 +2442,39 @@ body.discussion {
color:#000; color:#000;
font-style: italic; font-style: italic;
background-color:#fff; 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,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>)
......
...@@ -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,11 +48,12 @@ ...@@ -48,11 +48,12 @@
<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>
<!-- Template <!-- Template
<article id="SOME-JOB" class="job"> <article id="SOME-JOB" class="job">
<div class="inner-wrapper"> <div class="inner-wrapper">
<h3><strong>TITLE</strong></h3> <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