Commit 182ae7ae by Greg Price

Add endorsement info to marked answers in forum

Co-authored-by: jsa <jsa@edx.org>
parent 10063a31
...@@ -3,14 +3,38 @@ describe "ThreadResponseShowView", -> ...@@ -3,14 +3,38 @@ describe "ThreadResponseShowView", ->
DiscussionSpecHelper.setUpGlobals() DiscussionSpecHelper.setUpGlobals()
setFixtures( setFixtures(
""" """
<div class="discussion-post"> <script type="text/template" id="thread-response-show-template">
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false"> <a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false"></a>
<span class="plus-icon"/><span class="votes-count-number">0</span> <span class="sr">votes (click to vote)</span> <a
href="javascript:void(0)"
class="endorse-btn action-endorse <%= thread.get('thread_type') == 'question' ? 'mark-answer' : '' %>"
style="cursor: default; display: none;"
data-tooltip="<%= thread.get('thread_type') == 'question' ? 'mark as answer' : 'endorse' %>"
>
<span class="check-icon" style="pointer-events: none; "></span>
</a> </a>
</div> <p class="posted-details">
<span class="timeago" title="<%= created_at %>"><%= created_at %></span>
<% if (thread.get('thread_type') == 'question' && obj.endorsement) { %> -
<%=
interpolate(
endorsement.username ? "marked as answer %(time_ago)s by %(user)s" : "marked as answer %(time_ago)s",
{
'time_ago': '<span class="timeago" title="' + endorsement.time + '">' + endorsement.time + '</span>',
'user': endorsement.username
},
true
)
%>
<% } %>
</p>
</script>
<div class="discussion-post"></div>
""" """
) )
@thread = new Thread({"thread_type": "discussion"})
@commentData = { @commentData = {
id: "dummy", id: "dummy",
user_id: "567", user_id: "567",
...@@ -21,9 +45,15 @@ describe "ThreadResponseShowView", -> ...@@ -21,9 +45,15 @@ describe "ThreadResponseShowView", ->
votes: {up_count: "42"} votes: {up_count: "42"}
} }
@comment = new Comment(@commentData) @comment = new Comment(@commentData)
@comment.set("thread", @thread)
@view = new ThreadResponseShowView({ model: @comment }) @view = new ThreadResponseShowView({ model: @comment })
@view.setElement($(".discussion-post")) @view.setElement($(".discussion-post"))
# Avoid unnecessary boilerplate
spyOn(ThreadResponseShowView.prototype, "convertMath")
@view.render()
it "renders the vote correctly", -> it "renders the vote correctly", ->
DiscussionViewSpecHelper.checkRenderVote(@view, @comment) DiscussionViewSpecHelper.checkRenderVote(@view, @comment)
...@@ -38,3 +68,32 @@ describe "ThreadResponseShowView", -> ...@@ -38,3 +68,32 @@ describe "ThreadResponseShowView", ->
it "vote button activates on appropriate events", -> it "vote button activates on appropriate events", ->
DiscussionViewSpecHelper.checkVoteButtonEvents(@view) DiscussionViewSpecHelper.checkVoteButtonEvents(@view)
it "renders endorsement correctly for a marked answer in a question thread", ->
endorsement = {
"username": "test_endorser",
"time": new Date().toISOString()
}
@thread.set("thread_type", "question")
@comment.set({
"endorsed": true,
"endorsement": endorsement
})
@view.render()
expect(@view.$(".posted-details").text()).toMatch(
"marked as answer less than a minute ago by " + endorsement.username
)
it "renders anonymous endorsement correctly for a marked answer in a question thread", ->
endorsement = {
"username": null,
"time": new Date().toISOString()
}
@thread.set("thread_type", "question")
@comment.set({
"endorsed": true,
"endorsement": endorsement
})
@view.render()
expect(@view.$(".posted-details").text()).toMatch("marked as answer less than a minute ago")
expect(@view.$(".posted-details").text()).not.toMatch(" by ")
...@@ -29,7 +29,7 @@ if Backbone? ...@@ -29,7 +29,7 @@ if Backbone?
@renderVote() @renderVote()
@renderAttrs() @renderAttrs()
@renderFlagged() @renderFlagged()
@$el.find(".posted-details").timeago() @$el.find(".posted-details .timeago").timeago()
@convertMath() @convertMath()
@markAsStaff() @markAsStaff()
@ @
......
...@@ -53,11 +53,11 @@ def permitted(fn): ...@@ -53,11 +53,11 @@ def permitted(fn):
return wrapper return wrapper
def ajax_content_response(request, course_id, content): def ajax_content_response(request, course_key, content):
user_info = cc.User.from_django_user(request.user).to_dict() user_info = cc.User.from_django_user(request.user).to_dict()
annotated_content_info = get_annotated_content_info(course_id, content, request.user, user_info) annotated_content_info = get_annotated_content_info(course_key, content, request.user, user_info)
return JsonResponse({ return JsonResponse({
'content': safe_content(content), 'content': safe_content(content, course_key),
'annotated_content_info': annotated_content_info, 'annotated_content_info': annotated_content_info,
}) })
...@@ -71,8 +71,8 @@ def create_thread(request, course_id, commentable_id): ...@@ -71,8 +71,8 @@ def create_thread(request, course_id, commentable_id):
""" """
log.debug("Creating new thread in %r, id %r", course_id, commentable_id) log.debug("Creating new thread in %r, id %r", course_id, commentable_id)
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course = get_course_with_access(request.user, 'load', course_id) course = get_course_with_access(request.user, 'load', course_key)
post = request.POST post = request.POST
if course.allow_anonymous: if course.allow_anonymous:
...@@ -94,7 +94,7 @@ def create_thread(request, course_id, commentable_id): ...@@ -94,7 +94,7 @@ def create_thread(request, course_id, commentable_id):
anonymous=anonymous, anonymous=anonymous,
anonymous_to_peers=anonymous_to_peers, anonymous_to_peers=anonymous_to_peers,
commentable_id=commentable_id, commentable_id=commentable_id,
course_id=course_id.to_deprecated_string(), course_id=course_key.to_deprecated_string(),
user_id=request.user.id, user_id=request.user.id,
body=post["body"], body=post["body"],
title=post["title"] title=post["title"]
...@@ -107,13 +107,13 @@ def create_thread(request, course_id, commentable_id): ...@@ -107,13 +107,13 @@ def create_thread(request, course_id, commentable_id):
#not anymore, only for admins #not anymore, only for admins
# Cohort the thread if the commentable is cohorted. # Cohort the thread if the commentable is cohorted.
if is_commentable_cohorted(course_id, commentable_id): if is_commentable_cohorted(course_key, commentable_id):
user_group_id = get_cohort_id(user, course_id) user_group_id = get_cohort_id(user, course_key)
# TODO (vshnayder): once we have more than just cohorts, we'll want to # TODO (vshnayder): once we have more than just cohorts, we'll want to
# change this to a single get_group_for_user_and_commentable function # change this to a single get_group_for_user_and_commentable function
# that can do different things depending on the commentable_id # that can do different things depending on the commentable_id
if cached_has_permission(request.user, "see_all_cohorts", course_id): if cached_has_permission(request.user, "see_all_cohorts", course_key):
# admins can optionally choose what group to post as # admins can optionally choose what group to post as
group_id = post.get('group_id', user_group_id) group_id = post.get('group_id', user_group_id)
else: else:
...@@ -135,9 +135,9 @@ def create_thread(request, course_id, commentable_id): ...@@ -135,9 +135,9 @@ def create_thread(request, course_id, commentable_id):
data = thread.to_dict() data = thread.to_dict()
add_courseware_context([data], course) add_courseware_context([data], course)
if request.is_ajax(): if request.is_ajax():
return ajax_content_response(request, course_id, data) return ajax_content_response(request, course_key, data)
else: else:
return JsonResponse(safe_content(data)) return JsonResponse(safe_content(data, course_key))
@require_POST @require_POST
...@@ -151,19 +151,20 @@ def update_thread(request, course_id, thread_id): ...@@ -151,19 +151,20 @@ def update_thread(request, course_id, thread_id):
return JsonError(_("Title can't be empty")) return JsonError(_("Title can't be empty"))
if 'body' not in request.POST or not request.POST['body'].strip(): if 'body' not in request.POST or not request.POST['body'].strip():
return JsonError(_("Body can't be empty")) return JsonError(_("Body can't be empty"))
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
thread = cc.Thread.find(thread_id) thread = cc.Thread.find(thread_id)
thread.body = request.POST["body"] thread.body = request.POST["body"]
thread.title = request.POST["title"] thread.title = request.POST["title"]
thread.save() thread.save()
if request.is_ajax(): if request.is_ajax():
return ajax_content_response(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), thread.to_dict()) return ajax_content_response(request, course_key, thread.to_dict())
else: else:
return JsonResponse(safe_content(thread.to_dict())) return JsonResponse(safe_content(thread.to_dict(), course_key))
def _create_comment(request, course_key, thread_id=None, parent_id=None): def _create_comment(request, course_key, thread_id=None, parent_id=None):
""" """
given a course_id, thread_id, and parent_id, create a comment, given a course_key, thread_id, and parent_id, create a comment,
called from create_comment to do the actual creation called from create_comment to do the actual creation
""" """
assert isinstance(course_key, CourseKey) assert isinstance(course_key, CourseKey)
...@@ -199,7 +200,7 @@ def _create_comment(request, course_key, thread_id=None, parent_id=None): ...@@ -199,7 +200,7 @@ def _create_comment(request, course_key, thread_id=None, parent_id=None):
if request.is_ajax(): if request.is_ajax():
return ajax_content_response(request, course_key, comment.to_dict()) return ajax_content_response(request, course_key, comment.to_dict())
else: else:
return JsonResponse(safe_content(comment.to_dict())) return JsonResponse(safe_content(comment.to_dict(), course.id))
@require_POST @require_POST
...@@ -224,9 +225,10 @@ def delete_thread(request, course_id, thread_id): ...@@ -224,9 +225,10 @@ def delete_thread(request, course_id, thread_id):
given a course_id and thread_id, delete this thread given a course_id and thread_id, delete this thread
this is ajax only this is ajax only
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
thread = cc.Thread.find(thread_id) thread = cc.Thread.find(thread_id)
thread.delete() thread.delete()
return JsonResponse(safe_content(thread.to_dict())) return JsonResponse(safe_content(thread.to_dict(), course_key))
@require_POST @require_POST
...@@ -237,15 +239,16 @@ def update_comment(request, course_id, comment_id): ...@@ -237,15 +239,16 @@ def update_comment(request, course_id, comment_id):
given a course_id and comment_id, update the comment with payload attributes given a course_id and comment_id, update the comment with payload attributes
handles static and ajax submissions handles static and ajax submissions
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
comment = cc.Comment.find(comment_id) comment = cc.Comment.find(comment_id)
if 'body' not in request.POST or not request.POST['body'].strip(): if 'body' not in request.POST or not request.POST['body'].strip():
return JsonError(_("Body can't be empty")) return JsonError(_("Body can't be empty"))
comment.body = request.POST["body"] comment.body = request.POST["body"]
comment.save() comment.save()
if request.is_ajax(): if request.is_ajax():
return ajax_content_response(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), comment.to_dict()) return ajax_content_response(request, course_key, comment.to_dict())
else: else:
return JsonResponse(safe_content(comment.to_dict())) return JsonResponse(safe_content(comment.to_dict(), course_key))
@require_POST @require_POST
...@@ -256,10 +259,12 @@ def endorse_comment(request, course_id, comment_id): ...@@ -256,10 +259,12 @@ def endorse_comment(request, course_id, comment_id):
given a course_id and comment_id, toggle the endorsement of this comment, given a course_id and comment_id, toggle the endorsement of this comment,
ajax only ajax only
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
comment = cc.Comment.find(comment_id) comment = cc.Comment.find(comment_id)
comment.endorsed = request.POST.get('endorsed', 'false').lower() == 'true' comment.endorsed = request.POST.get('endorsed', 'false').lower() == 'true'
comment.endorsement_user_id = request.user.id
comment.save() comment.save()
return JsonResponse(safe_content(comment.to_dict())) return JsonResponse(safe_content(comment.to_dict(), course_key))
@require_POST @require_POST
...@@ -270,13 +275,14 @@ def openclose_thread(request, course_id, thread_id): ...@@ -270,13 +275,14 @@ def openclose_thread(request, course_id, thread_id):
given a course_id and thread_id, toggle the status of this thread given a course_id and thread_id, toggle the status of this thread
ajax only ajax only
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
thread = cc.Thread.find(thread_id) thread = cc.Thread.find(thread_id)
thread.closed = request.POST.get('closed', 'false').lower() == 'true' thread.closed = request.POST.get('closed', 'false').lower() == 'true'
thread.save() thread.save()
thread = thread.to_dict() thread = thread.to_dict()
return JsonResponse({ return JsonResponse({
'content': safe_content(thread), 'content': safe_content(thread, course_key),
'ability': get_ability(SlashSeparatedCourseKey.from_deprecated_string(course_id), thread, request.user), 'ability': get_ability(course_key, thread, request.user),
}) })
...@@ -302,9 +308,10 @@ def delete_comment(request, course_id, comment_id): ...@@ -302,9 +308,10 @@ def delete_comment(request, course_id, comment_id):
given a course_id and comment_id delete this comment given a course_id and comment_id delete this comment
ajax only ajax only
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
comment = cc.Comment.find(comment_id) comment = cc.Comment.find(comment_id)
comment.delete() comment.delete()
return JsonResponse(safe_content(comment.to_dict())) return JsonResponse(safe_content(comment.to_dict(), course_key))
@require_POST @require_POST
...@@ -314,10 +321,11 @@ def vote_for_comment(request, course_id, comment_id, value): ...@@ -314,10 +321,11 @@ def vote_for_comment(request, course_id, comment_id, value):
""" """
given a course_id and comment_id, given a course_id and comment_id,
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
comment = cc.Comment.find(comment_id) comment = cc.Comment.find(comment_id)
user.vote(comment, value) user.vote(comment, value)
return JsonResponse(safe_content(comment.to_dict())) return JsonResponse(safe_content(comment.to_dict(), course_key))
@require_POST @require_POST
...@@ -328,10 +336,11 @@ def undo_vote_for_comment(request, course_id, comment_id): ...@@ -328,10 +336,11 @@ def undo_vote_for_comment(request, course_id, comment_id):
given a course id and comment id, remove vote given a course id and comment id, remove vote
ajax only ajax only
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
comment = cc.Comment.find(comment_id) comment = cc.Comment.find(comment_id)
user.unvote(comment) user.unvote(comment)
return JsonResponse(safe_content(comment.to_dict())) return JsonResponse(safe_content(comment.to_dict(), course_key))
@require_POST @require_POST
...@@ -342,10 +351,11 @@ def vote_for_thread(request, course_id, thread_id, value): ...@@ -342,10 +351,11 @@ def vote_for_thread(request, course_id, thread_id, value):
given a course id and thread id vote for this thread given a course id and thread id vote for this thread
ajax only ajax only
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
thread = cc.Thread.find(thread_id) thread = cc.Thread.find(thread_id)
user.vote(thread, value) user.vote(thread, value)
return JsonResponse(safe_content(thread.to_dict())) return JsonResponse(safe_content(thread.to_dict(), course_key))
@require_POST @require_POST
...@@ -356,10 +366,11 @@ def flag_abuse_for_thread(request, course_id, thread_id): ...@@ -356,10 +366,11 @@ def flag_abuse_for_thread(request, course_id, thread_id):
given a course_id and thread_id flag this thread for abuse given a course_id and thread_id flag this thread for abuse
ajax only ajax only
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
thread = cc.Thread.find(thread_id) thread = cc.Thread.find(thread_id)
thread.flagAbuse(user, thread) thread.flagAbuse(user, thread)
return JsonResponse(safe_content(thread.to_dict())) return JsonResponse(safe_content(thread.to_dict(), course_key))
@require_POST @require_POST
...@@ -371,12 +382,12 @@ def un_flag_abuse_for_thread(request, course_id, thread_id): ...@@ -371,12 +382,12 @@ def un_flag_abuse_for_thread(request, course_id, thread_id):
ajax only ajax only
""" """
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course = get_course_by_id(course_id) course = get_course_by_id(course_key)
thread = cc.Thread.find(thread_id) thread = cc.Thread.find(thread_id)
remove_all = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, 'staff', course) remove_all = cached_has_permission(request.user, 'openclose_thread', course_key) or has_access(request.user, 'staff', course)
thread.unFlagAbuse(user, thread, remove_all) thread.unFlagAbuse(user, thread, remove_all)
return JsonResponse(safe_content(thread.to_dict())) return JsonResponse(safe_content(thread.to_dict(), course_key))
@require_POST @require_POST
...@@ -387,10 +398,11 @@ def flag_abuse_for_comment(request, course_id, comment_id): ...@@ -387,10 +398,11 @@ def flag_abuse_for_comment(request, course_id, comment_id):
given a course and comment id, flag comment for abuse given a course and comment id, flag comment for abuse
ajax only ajax only
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
comment = cc.Comment.find(comment_id) comment = cc.Comment.find(comment_id)
comment.flagAbuse(user, comment) comment.flagAbuse(user, comment)
return JsonResponse(safe_content(comment.to_dict())) return JsonResponse(safe_content(comment.to_dict(), course_key))
@require_POST @require_POST
...@@ -407,7 +419,7 @@ def un_flag_abuse_for_comment(request, course_id, comment_id): ...@@ -407,7 +419,7 @@ def un_flag_abuse_for_comment(request, course_id, comment_id):
remove_all = cached_has_permission(request.user, 'openclose_thread', course_key) or has_access(request.user, 'staff', course) remove_all = cached_has_permission(request.user, 'openclose_thread', course_key) or has_access(request.user, 'staff', course)
comment = cc.Comment.find(comment_id) comment = cc.Comment.find(comment_id)
comment.unFlagAbuse(user, comment, remove_all) comment.unFlagAbuse(user, comment, remove_all)
return JsonResponse(safe_content(comment.to_dict())) return JsonResponse(safe_content(comment.to_dict(), course_key))
@require_POST @require_POST
...@@ -418,10 +430,11 @@ def undo_vote_for_thread(request, course_id, thread_id): ...@@ -418,10 +430,11 @@ def undo_vote_for_thread(request, course_id, thread_id):
given a course id and thread id, remove users vote for thread given a course id and thread id, remove users vote for thread
ajax only ajax only
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
thread = cc.Thread.find(thread_id) thread = cc.Thread.find(thread_id)
user.unvote(thread) user.unvote(thread)
return JsonResponse(safe_content(thread.to_dict())) return JsonResponse(safe_content(thread.to_dict(), course_key))
@require_POST @require_POST
...@@ -432,10 +445,11 @@ def pin_thread(request, course_id, thread_id): ...@@ -432,10 +445,11 @@ def pin_thread(request, course_id, thread_id):
given a course id and thread id, pin this thread given a course id and thread id, pin this thread
ajax only ajax only
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
thread = cc.Thread.find(thread_id) thread = cc.Thread.find(thread_id)
thread.pin(user, thread_id) thread.pin(user, thread_id)
return JsonResponse(safe_content(thread.to_dict())) return JsonResponse(safe_content(thread.to_dict(), course_key))
@require_POST @require_POST
...@@ -446,10 +460,11 @@ def un_pin_thread(request, course_id, thread_id): ...@@ -446,10 +460,11 @@ def un_pin_thread(request, course_id, thread_id):
given a course id and thread id, remove pin from this thread given a course id and thread id, remove pin from this thread
ajax only ajax only
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
thread = cc.Thread.find(thread_id) thread = cc.Thread.find(thread_id)
thread.un_pin(user, thread_id) thread.un_pin(user, thread_id)
return JsonResponse(safe_content(thread.to_dict())) return JsonResponse(safe_content(thread.to_dict(), course_key))
@require_POST @require_POST
......
...@@ -150,7 +150,7 @@ def inline_discussion(request, course_id, discussion_id): ...@@ -150,7 +150,7 @@ def inline_discussion(request, course_id, discussion_id):
annotated_content_info = utils.get_metadata_for_threads(course_id, threads, request.user, user_info) annotated_content_info = utils.get_metadata_for_threads(course_id, threads, request.user, user_info)
is_staff = cached_has_permission(request.user, 'openclose_thread', course.id) is_staff = cached_has_permission(request.user, 'openclose_thread', course.id)
return utils.JsonResponse({ return utils.JsonResponse({
'discussion_data': [utils.safe_content(thread, is_staff) for thread in threads], 'discussion_data': [utils.safe_content(thread, course_id, is_staff) for thread in threads],
'user_info': user_info, 'user_info': user_info,
'annotated_content_info': annotated_content_info, 'annotated_content_info': annotated_content_info,
'page': query_params['page'], 'page': query_params['page'],
...@@ -173,7 +173,7 @@ def forum_form_discussion(request, course_id): ...@@ -173,7 +173,7 @@ def forum_form_discussion(request, course_id):
try: try:
unsafethreads, query_params = get_threads(request, course_id) # This might process a search query unsafethreads, query_params = get_threads(request, course_id) # This might process a search query
is_staff = cached_has_permission(request.user, 'openclose_thread', course.id) is_staff = cached_has_permission(request.user, 'openclose_thread', course.id)
threads = [utils.safe_content(thread, is_staff) for thread in unsafethreads] threads = [utils.safe_content(thread, course_id, is_staff) for thread in unsafethreads]
except cc.utils.CommentClientMaintenanceError: except cc.utils.CommentClientMaintenanceError:
log.warning("Forum is in maintenance mode") log.warning("Forum is in maintenance mode")
return render_to_response('discussion/maintenance.html', {}) return render_to_response('discussion/maintenance.html', {})
...@@ -253,7 +253,7 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -253,7 +253,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
if request.is_ajax(): if request.is_ajax():
with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"): with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"):
annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, user_info=user_info) annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, user_info=user_info)
content = utils.safe_content(thread.to_dict(), is_staff) content = utils.safe_content(thread.to_dict(), course_id, is_staff)
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
add_courseware_context([content], course) add_courseware_context([content], course)
return utils.JsonResponse({ return utils.JsonResponse({
...@@ -276,7 +276,7 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -276,7 +276,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
if not "pinned" in thread: if not "pinned" in thread:
thread["pinned"] = False thread["pinned"] = False
threads = [utils.safe_content(thread, is_staff) for thread in threads] threads = [utils.safe_content(thread, course_id, is_staff) for thread in threads]
with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"):
annotated_content_info = utils.get_metadata_for_threads(course_id, threads, request.user, user_info) annotated_content_info = utils.get_metadata_for_threads(course_id, threads, request.user, user_info)
...@@ -335,7 +335,7 @@ def user_profile(request, course_id, user_id): ...@@ -335,7 +335,7 @@ def user_profile(request, course_id, user_id):
if request.is_ajax(): if request.is_ajax():
is_staff = cached_has_permission(request.user, 'openclose_thread', course.id) is_staff = cached_has_permission(request.user, 'openclose_thread', course.id)
return utils.JsonResponse({ return utils.JsonResponse({
'discussion_data': [utils.safe_content(thread, is_staff) for thread in threads], 'discussion_data': [utils.safe_content(thread, course_id, is_staff) for thread in threads],
'page': query_params['page'], 'page': query_params['page'],
'num_pages': query_params['num_pages'], 'num_pages': query_params['num_pages'],
'annotated_content_info': _attr_safe_json(annotated_content_info), 'annotated_content_info': _attr_safe_json(annotated_content_info),
...@@ -386,7 +386,7 @@ def followed_threads(request, course_id, user_id): ...@@ -386,7 +386,7 @@ def followed_threads(request, course_id, user_id):
is_staff = cached_has_permission(request.user, 'openclose_thread', course.id) is_staff = cached_has_permission(request.user, 'openclose_thread', course.id)
return utils.JsonResponse({ return utils.JsonResponse({
'annotated_content_info': annotated_content_info, 'annotated_content_info': annotated_content_info,
'discussion_data': [utils.safe_content(thread, is_staff) for thread in threads], 'discussion_data': [utils.safe_content(thread, course_id, is_staff) for thread in threads],
'page': query_params['page'], 'page': query_params['page'],
'num_pages': query_params['num_pages'], 'num_pages': query_params['num_pages'],
}) })
......
...@@ -9,7 +9,7 @@ from django.db import connection ...@@ -9,7 +9,7 @@ from django.db import connection
from django.http import HttpResponse from django.http import HttpResponse
from django.utils import simplejson from django.utils import simplejson
from django_comment_common.models import Role, FORUM_ROLE_STUDENT from django_comment_common.models import Role, FORUM_ROLE_STUDENT
from django_comment_client.permissions import check_permissions_by_view from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
from edxmako import lookup_template from edxmako import lookup_template
import pystache_custom as pystache import pystache_custom as pystache
...@@ -365,7 +365,7 @@ def add_courseware_context(content_list, course): ...@@ -365,7 +365,7 @@ def add_courseware_context(content_list, course):
content.update({"courseware_url": url, "courseware_title": title}) content.update({"courseware_url": url, "courseware_title": title})
def safe_content(content, is_staff=False): def safe_content(content, course_id, is_staff=False):
fields = [ fields = [
'id', 'title', 'body', 'course_id', 'anonymous', 'anonymous_to_peers', 'id', 'title', 'body', 'course_id', 'anonymous', 'anonymous_to_peers',
'endorsed', 'parent_id', 'thread_id', 'votes', 'closed', 'created_at', 'endorsed', 'parent_id', 'thread_id', 'votes', 'closed', 'created_at',
...@@ -375,14 +375,40 @@ def safe_content(content, is_staff=False): ...@@ -375,14 +375,40 @@ def safe_content(content, is_staff=False):
'read', 'group_id', 'group_name', 'group_string', 'pinned', 'abuse_flaggers', 'read', 'group_id', 'group_name', 'group_string', 'pinned', 'abuse_flaggers',
'stats', 'resp_skip', 'resp_limit', 'resp_total', 'thread_type', 'stats', 'resp_skip', 'resp_limit', 'resp_total', 'thread_type',
'endorsed_responses', 'non_endorsed_responses', 'non_endorsed_resp_total', 'endorsed_responses', 'non_endorsed_responses', 'non_endorsed_resp_total',
'endorsement',
] ]
if (content.get('anonymous') is False) and ((content.get('anonymous_to_peers') is False) or is_staff): if (content.get('anonymous') is False) and ((content.get('anonymous_to_peers') is False) or is_staff):
fields += ['username', 'user_id'] fields += ['username', 'user_id']
content = strip_none(extract(content, fields))
if content.get("endorsement"):
endorsement = content["endorsement"]
endorser = None
if endorsement["user_id"]:
try:
endorser = User.objects.get(pk=endorsement["user_id"])
except User.DoesNotExist:
log.error("User ID {0} in endorsement for comment {1} but not in our DB.".format(
content.get('user_id'),
content.get('id'))
)
# Only reveal endorser if requester can see author or if endorser is staff
if (
endorser and
("username" in fields or cached_has_permission(endorser, "endorse_comment", course_id))
):
endorsement["username"] = endorser.username
else:
del endorsement["user_id"]
for child_content_key in ["children", "endorsed_responses", "non_endorsed_responses"]: for child_content_key in ["children", "endorsed_responses", "non_endorsed_responses"]:
if child_content_key in content: if child_content_key in content:
safe_children = [safe_content(child) for child in content[child_content_key]] safe_children = [
safe_content(child, course_id, is_staff) for child in content[child_content_key]
]
content[child_content_key] = safe_children content[child_content_key] = safe_children
return strip_none(extract(content, fields)) return content
...@@ -11,12 +11,12 @@ class Comment(models.Model): ...@@ -11,12 +11,12 @@ class Comment(models.Model):
'id', 'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'id', 'body', 'anonymous', 'anonymous_to_peers', 'course_id',
'endorsed', 'parent_id', 'thread_id', 'username', 'votes', 'user_id', 'endorsed', 'parent_id', 'thread_id', 'username', 'votes', 'user_id',
'closed', 'created_at', 'updated_at', 'depth', 'at_position_list', 'closed', 'created_at', 'updated_at', 'depth', 'at_position_list',
'type', 'commentable_id', 'abuse_flaggers' 'type', 'commentable_id', 'abuse_flaggers', 'endorsement',
] ]
updatable_fields = [ updatable_fields = [
'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'closed', 'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'closed',
'user_id', 'endorsed' 'user_id', 'endorsed', 'endorsement_user_id',
] ]
initializable_fields = updatable_fields initializable_fields = updatable_fields
......
...@@ -35,7 +35,7 @@ class Model(object): ...@@ -35,7 +35,7 @@ class Model(object):
return self.__getattr__(name) return self.__getattr__(name)
def __setattr__(self, name, value): def __setattr__(self, name, value):
if name == 'attributes' or name not in self.accessible_fields: if name == 'attributes' or name not in (self.accessible_fields + self.updatable_fields):
super(Model, self).__setattr__(name, value) super(Model, self).__setattr__(name, value)
else: else:
self.attributes[name] = value self.attributes[name] = value
...@@ -46,7 +46,7 @@ class Model(object): ...@@ -46,7 +46,7 @@ class Model(object):
return self.attributes.get(key) return self.attributes.get(key)
def __setitem__(self, key, value): def __setitem__(self, key, value):
if key not in self.accessible_fields: if key not in (self.accessible_fields + self.updatable_fields):
raise KeyError("Field {0} does not exist".format(key)) raise KeyError("Field {0} does not exist".format(key))
self.attributes.__setitem__(key, value) self.attributes.__setitem__(key, value)
......
...@@ -441,7 +441,8 @@ body.discussion { ...@@ -441,7 +441,8 @@ body.discussion {
font-weight: 700; font-weight: 700;
} }
span { .timeago, .top-post-status {
color: inherit;
font-style: italic; font-style: italic;
} }
} }
......
...@@ -159,7 +159,28 @@ ...@@ -159,7 +159,28 @@
${"<% } else { %>"} ${"<% } else { %>"}
<span class="anonymous"><em>${_('anonymous')}</em></span> <span class="anonymous"><em>${_('anonymous')}</em></span>
${"<% } %>"} ${"<% } %>"}
<p class="posted-details" title="${'<%- created_at %>'}">${'<%- created_at %>'}</p> <p class="posted-details">
<span class="timeago" title="${'<%= created_at %>'}">${'<%= created_at %>'}</span>
<%
js_block = u"""
interpolate(
endorsement.username ? "{user_fmt_str}" : "{anon_fmt_str}",
{{
'time_ago': '<span class="timeago" title="' + endorsement.time + '">' + endorsement.time + '</span>',
'user': endorsement.username
}},
true
)""".format(
## Translators: time_ago is a placeholder for a fuzzy, relative timestamp
## like "4 hours ago" or "about a month ago"
user_fmt_str=escapejs(_("marked as answer %(time_ago)s by %(user)s")),
## Translators: time_ago is a placeholder for a fuzzy, relative timestamp
## like "4 hours ago" or "about a month ago"
anon_fmt_str=escapejs(_("marked as answer %(time_ago)s")),
)
%>
${"<% if (thread.get('thread_type') == 'question' && obj.endorsement) { %> - <%="}${js_block}${"%><% } %>"}
</p>
</header> </header>
<div class="response-body">${"<%- body %>"}</div> <div class="response-body">${"<%- body %>"}</div>
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" role="button" aria-pressed="false" tabindex="0"> <div class="discussion-flag-abuse notflagged" data-role="thread-flag" role="button" aria-pressed="false" tabindex="0">
......
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