Merge pull request #1683 from MITx/feature/kevin/pinning_patch
Feature/kevin/pinning patch
1
.gitignore
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -356,7 +356,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
valid_get_dict = self._querydict_from_dict({'input_2[]': ['test1', 'test2']})
|
||||
result = CapaModule.make_dict_of_responses(valid_get_dict)
|
||||
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
|
||||
# get a list, even if there's just one value
|
||||
@@ -374,7 +374,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# One of the values would overwrite the other, so detect this
|
||||
# and raise an exception
|
||||
invalid_get_dict = self._querydict_from_dict({'input_1[]': 'test 1',
|
||||
'input_1': 'test 2' })
|
||||
'input_1': 'test 2'})
|
||||
with self.assertRaises(ValueError):
|
||||
result = CapaModule.make_dict_of_responses(invalid_get_dict)
|
||||
|
||||
@@ -412,7 +412,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
mock_html.return_value = "Test HTML"
|
||||
|
||||
# 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)
|
||||
|
||||
# Expect that the problem is marked correct
|
||||
@@ -434,7 +434,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
mock_is_correct.return_value = False
|
||||
|
||||
# Check the problem
|
||||
get_request_dict = { CapaFactory.input_key(): '0' }
|
||||
get_request_dict = { CapaFactory.input_key(): '0'}
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
# Expect that the problem is marked correct
|
||||
@@ -452,7 +452,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
|
||||
mock_closed.return_value = True
|
||||
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)
|
||||
|
||||
# Expect that number of attempts NOT incremented
|
||||
@@ -468,7 +468,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
|
||||
# Expect that we cannot submit
|
||||
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)
|
||||
|
||||
# Expect that number of attempts NOT incremented
|
||||
@@ -483,7 +483,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
module.lcp.done = True
|
||||
|
||||
# Expect that we can submit successfully
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14' }
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14'}
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
self.assertEqual(result['success'], 'correct')
|
||||
@@ -504,7 +504,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
mock_is_queued.return_value = True
|
||||
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)
|
||||
|
||||
# Expect an AJAX alert message in 'success'
|
||||
@@ -521,7 +521,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
|
||||
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)
|
||||
|
||||
# Expect an AJAX alert message in 'success'
|
||||
@@ -595,11 +595,11 @@ class CapaModuleTest(unittest.TestCase):
|
||||
module.lcp.done = False
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
||||
# Expect that the result is success
|
||||
@@ -617,7 +617,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
mock_closed.return_value = True
|
||||
|
||||
# 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)
|
||||
|
||||
# Expect that the result is failure
|
||||
@@ -631,7 +631,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
module.lcp.done = True
|
||||
|
||||
# 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)
|
||||
|
||||
# Expect that we cannot save
|
||||
@@ -645,7 +645,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
module.lcp.done = True
|
||||
|
||||
# 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)
|
||||
|
||||
# Expect that we succeed
|
||||
@@ -657,7 +657,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Just in case, we also check what happens if we have
|
||||
# more attempts than allowed.
|
||||
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")
|
||||
|
||||
module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
|
||||
@@ -667,14 +667,14 @@ class CapaModuleTest(unittest.TestCase):
|
||||
self.assertEqual(module.check_button_name(), "Final 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")
|
||||
|
||||
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")
|
||||
|
||||
# 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")
|
||||
|
||||
module = CapaFactory.create(attempts=0)
|
||||
@@ -682,7 +682,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
|
||||
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
|
||||
module = CapaFactory.create(due=self.yesterday_str)
|
||||
@@ -718,7 +718,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
|
||||
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
|
||||
module = CapaFactory.create(due=self.yesterday_str)
|
||||
@@ -755,7 +755,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
|
||||
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
|
||||
module = CapaFactory.create(due=self.yesterday_str)
|
||||
@@ -772,13 +772,24 @@ class CapaModuleTest(unittest.TestCase):
|
||||
module.lcp.done = True
|
||||
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
|
||||
module = CapaFactory.create()
|
||||
module.lcp.done = False
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
# If we're not randomizing, then we can re-save
|
||||
module = CapaFactory.create(rerandomize="never")
|
||||
# If we're not randomizing and we have limited attempts, then we can save
|
||||
module = CapaFactory.create(rerandomize="never", max_attempts=2)
|
||||
module.lcp.done = True
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
@@ -797,7 +808,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
# If the user is out of attempts, do NOT show the save button
|
||||
attempts = random.randint(1,10)
|
||||
attempts = random.randint(1, 10)
|
||||
module = CapaFactory.create(attempts=attempts,
|
||||
max_attempts=attempts,
|
||||
force_save_button="true")
|
||||
@@ -823,9 +834,9 @@ class CapaModuleTest(unittest.TestCase):
|
||||
|
||||
# We've tested the show/hide button logic in other tests,
|
||||
# so here we hard-wire the values
|
||||
show_check_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_check_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)
|
||||
|
||||
module.should_show_check_button = Mock(return_value=show_check_button)
|
||||
module.should_show_reset_button = Mock(return_value=show_reset_button)
|
||||
@@ -848,7 +859,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
self.assertEqual(html, "<div>Test Template HTML</div>")
|
||||
|
||||
# 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)
|
||||
|
||||
template_name = render_args[0]
|
||||
@@ -889,7 +900,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
html = module.get_problem_html()
|
||||
|
||||
# Check the rendering context
|
||||
render_args,_ = module.system.render_template.call_args
|
||||
render_args, _ = module.system.render_template.call_args
|
||||
context = render_args[1]
|
||||
self.assertTrue("error" in context['problem']['html'])
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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();
|
||||
BIN
common/static/js/capa/genex/images/circles.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
common/static/js/capa/genex/images/circles_ie6.png
Normal file
|
After Width: | Height: | Size: 432 B |
BIN
common/static/js/capa/genex/images/corner.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
common/static/js/capa/genex/images/corner_ie6.png
Normal file
|
After Width: | Height: | Size: 412 B |
BIN
common/static/js/capa/genex/images/hborder.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
common/static/js/capa/genex/images/hborder_ie6.png
Normal file
|
After Width: | Height: | Size: 706 B |
BIN
common/static/js/capa/genex/images/thumb_horz.png
Normal file
|
After Width: | Height: | Size: 222 B |
BIN
common/static/js/capa/genex/images/thumb_vertical.png
Normal file
|
After Width: | Height: | Size: 231 B |
BIN
common/static/js/capa/genex/images/vborder.png
Normal file
|
After Width: | Height: | Size: 298 B |
BIN
common/static/js/capa/genex/images/vborder_ie6.png
Normal file
|
After Width: | Height: | Size: 189 B |
@@ -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):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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)
|
||||
|
||||
BIN
lms/static/images/pinned.png
Normal file
|
After Width: | Height: | Size: 518 B |
BIN
lms/static/images/press/releases/201x_240x180.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
lms/static/images/unpinned.png
Normal file
|
After Width: | Height: | Size: 498 B |
@@ -2442,4 +2442,39 @@ body.discussion {
|
||||
color:#000;
|
||||
font-style: italic;
|
||||
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;
|
||||
}
|
||||
@@ -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"><img src="${static.url('images/press/releases/201x_240x180.jpg')}" />
|
||||
<p></p></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 @@
|
||||
<p></p></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 @@
|
||||
<p></p></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 @@
|
||||
<p></p></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’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>
|
||||
|
||||