Add endorsement info to marked answers in forum
Co-authored-by: jsa <jsa@edx.org>
This commit is contained in:
@@ -3,14 +3,38 @@ describe "ThreadResponseShowView", ->
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
setFixtures(
|
||||
"""
|
||||
<div class="discussion-post">
|
||||
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
|
||||
<span class="plus-icon"/><span class="votes-count-number">0</span> <span class="sr">votes (click to vote)</span>
|
||||
<script type="text/template" id="thread-response-show-template">
|
||||
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false"></a>
|
||||
<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>
|
||||
</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 = {
|
||||
id: "dummy",
|
||||
user_id: "567",
|
||||
@@ -21,9 +45,15 @@ describe "ThreadResponseShowView", ->
|
||||
votes: {up_count: "42"}
|
||||
}
|
||||
@comment = new Comment(@commentData)
|
||||
@comment.set("thread", @thread)
|
||||
@view = new ThreadResponseShowView({ model: @comment })
|
||||
@view.setElement($(".discussion-post"))
|
||||
|
||||
# Avoid unnecessary boilerplate
|
||||
spyOn(ThreadResponseShowView.prototype, "convertMath")
|
||||
|
||||
@view.render()
|
||||
|
||||
it "renders the vote correctly", ->
|
||||
DiscussionViewSpecHelper.checkRenderVote(@view, @comment)
|
||||
|
||||
@@ -38,3 +68,32 @@ describe "ThreadResponseShowView", ->
|
||||
|
||||
it "vote button activates on appropriate events", ->
|
||||
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?
|
||||
@renderVote()
|
||||
@renderAttrs()
|
||||
@renderFlagged()
|
||||
@$el.find(".posted-details").timeago()
|
||||
@$el.find(".posted-details .timeago").timeago()
|
||||
@convertMath()
|
||||
@markAsStaff()
|
||||
@
|
||||
|
||||
@@ -53,11 +53,11 @@ def permitted(fn):
|
||||
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()
|
||||
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({
|
||||
'content': safe_content(content),
|
||||
'content': safe_content(content, course_key),
|
||||
'annotated_content_info': annotated_content_info,
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_id)
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
post = request.POST
|
||||
|
||||
if course.allow_anonymous:
|
||||
@@ -94,7 +94,7 @@ def create_thread(request, course_id, commentable_id):
|
||||
anonymous=anonymous,
|
||||
anonymous_to_peers=anonymous_to_peers,
|
||||
commentable_id=commentable_id,
|
||||
course_id=course_id.to_deprecated_string(),
|
||||
course_id=course_key.to_deprecated_string(),
|
||||
user_id=request.user.id,
|
||||
body=post["body"],
|
||||
title=post["title"]
|
||||
@@ -107,13 +107,13 @@ def create_thread(request, course_id, commentable_id):
|
||||
#not anymore, only for admins
|
||||
|
||||
# Cohort the thread if the commentable is cohorted.
|
||||
if is_commentable_cohorted(course_id, commentable_id):
|
||||
user_group_id = get_cohort_id(user, course_id)
|
||||
if is_commentable_cohorted(course_key, commentable_id):
|
||||
user_group_id = get_cohort_id(user, course_key)
|
||||
|
||||
# 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
|
||||
# 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
|
||||
group_id = post.get('group_id', user_group_id)
|
||||
else:
|
||||
@@ -135,9 +135,9 @@ def create_thread(request, course_id, commentable_id):
|
||||
data = thread.to_dict()
|
||||
add_courseware_context([data], course)
|
||||
if request.is_ajax():
|
||||
return ajax_content_response(request, course_id, data)
|
||||
return ajax_content_response(request, course_key, data)
|
||||
else:
|
||||
return JsonResponse(safe_content(data))
|
||||
return JsonResponse(safe_content(data, course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -151,19 +151,20 @@ def update_thread(request, course_id, thread_id):
|
||||
return JsonError(_("Title can't be empty"))
|
||||
if 'body' not in request.POST or not request.POST['body'].strip():
|
||||
return JsonError(_("Body can't be empty"))
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.body = request.POST["body"]
|
||||
thread.title = request.POST["title"]
|
||||
thread.save()
|
||||
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:
|
||||
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):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
assert isinstance(course_key, CourseKey)
|
||||
@@ -199,7 +200,7 @@ def _create_comment(request, course_key, thread_id=None, parent_id=None):
|
||||
if request.is_ajax():
|
||||
return ajax_content_response(request, course_key, comment.to_dict())
|
||||
else:
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict(), course.id))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -224,9 +225,10 @@ def delete_thread(request, course_id, thread_id):
|
||||
given a course_id and thread_id, delete this thread
|
||||
this is ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.delete()
|
||||
return JsonResponse(safe_content(thread.to_dict()))
|
||||
return JsonResponse(safe_content(thread.to_dict(), course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -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
|
||||
handles static and ajax submissions
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
if 'body' not in request.POST or not request.POST['body'].strip():
|
||||
return JsonError(_("Body can't be empty"))
|
||||
comment.body = request.POST["body"]
|
||||
comment.save()
|
||||
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:
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict(), course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -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,
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.endorsed = request.POST.get('endorsed', 'false').lower() == 'true'
|
||||
comment.endorsement_user_id = request.user.id
|
||||
comment.save()
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict(), course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -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
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.closed = request.POST.get('closed', 'false').lower() == 'true'
|
||||
thread.save()
|
||||
thread = thread.to_dict()
|
||||
return JsonResponse({
|
||||
'content': safe_content(thread),
|
||||
'ability': get_ability(SlashSeparatedCourseKey.from_deprecated_string(course_id), thread, request.user),
|
||||
'content': safe_content(thread, course_key),
|
||||
'ability': get_ability(course_key, thread, request.user),
|
||||
})
|
||||
|
||||
|
||||
@@ -302,9 +308,10 @@ def delete_comment(request, course_id, comment_id):
|
||||
given a course_id and comment_id delete this comment
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.delete()
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict(), course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -314,10 +321,11 @@ def vote_for_comment(request, course_id, comment_id, value):
|
||||
"""
|
||||
given a course_id and comment_id,
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
user.vote(comment, value)
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict(), course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -328,10 +336,11 @@ def undo_vote_for_comment(request, course_id, comment_id):
|
||||
given a course id and comment id, remove vote
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
user.unvote(comment)
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict(), course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -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
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
user.vote(thread, value)
|
||||
return JsonResponse(safe_content(thread.to_dict()))
|
||||
return JsonResponse(safe_content(thread.to_dict(), course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -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
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.flagAbuse(user, thread)
|
||||
return JsonResponse(safe_content(thread.to_dict()))
|
||||
return JsonResponse(safe_content(thread.to_dict(), course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -371,12 +382,12 @@ def un_flag_abuse_for_thread(request, course_id, thread_id):
|
||||
ajax only
|
||||
"""
|
||||
user = cc.User.from_django_user(request.user)
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_by_id(course_id)
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_by_id(course_key)
|
||||
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)
|
||||
return JsonResponse(safe_content(thread.to_dict()))
|
||||
return JsonResponse(safe_content(thread.to_dict(), course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -387,10 +398,11 @@ def flag_abuse_for_comment(request, course_id, comment_id):
|
||||
given a course and comment id, flag comment for abuse
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.flagAbuse(user, comment)
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict(), course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -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)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.unFlagAbuse(user, comment, remove_all)
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict(), course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -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
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
user.unvote(thread)
|
||||
return JsonResponse(safe_content(thread.to_dict()))
|
||||
return JsonResponse(safe_content(thread.to_dict(), course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -432,10 +445,11 @@ def pin_thread(request, course_id, thread_id):
|
||||
given a course id and thread id, pin this thread
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(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
|
||||
@@ -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
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(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
|
||||
|
||||
@@ -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)
|
||||
is_staff = cached_has_permission(request.user, 'openclose_thread', course.id)
|
||||
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,
|
||||
'annotated_content_info': annotated_content_info,
|
||||
'page': query_params['page'],
|
||||
@@ -173,7 +173,7 @@ def forum_form_discussion(request, course_id):
|
||||
try:
|
||||
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)
|
||||
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:
|
||||
log.warning("Forum is in maintenance mode")
|
||||
return render_to_response('discussion/maintenance.html', {})
|
||||
@@ -253,7 +253,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
if request.is_ajax():
|
||||
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)
|
||||
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"):
|
||||
add_courseware_context([content], course)
|
||||
return utils.JsonResponse({
|
||||
@@ -276,7 +276,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
if not "pinned" in thread:
|
||||
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"):
|
||||
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):
|
||||
if request.is_ajax():
|
||||
is_staff = cached_has_permission(request.user, 'openclose_thread', course.id)
|
||||
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'],
|
||||
'num_pages': query_params['num_pages'],
|
||||
'annotated_content_info': _attr_safe_json(annotated_content_info),
|
||||
@@ -386,7 +386,7 @@ def followed_threads(request, course_id, user_id):
|
||||
is_staff = cached_has_permission(request.user, 'openclose_thread', course.id)
|
||||
return utils.JsonResponse({
|
||||
'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'],
|
||||
'num_pages': query_params['num_pages'],
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.db import connection
|
||||
from django.http import HttpResponse
|
||||
from django.utils import simplejson
|
||||
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
|
||||
import pystache_custom as pystache
|
||||
@@ -365,7 +365,7 @@ def add_courseware_context(content_list, course):
|
||||
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 = [
|
||||
'id', 'title', 'body', 'course_id', 'anonymous', 'anonymous_to_peers',
|
||||
'endorsed', 'parent_id', 'thread_id', 'votes', 'closed', 'created_at',
|
||||
@@ -375,14 +375,40 @@ def safe_content(content, is_staff=False):
|
||||
'read', 'group_id', 'group_name', 'group_string', 'pinned', 'abuse_flaggers',
|
||||
'stats', 'resp_skip', 'resp_limit', 'resp_total', 'thread_type',
|
||||
'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):
|
||||
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"]:
|
||||
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
|
||||
|
||||
return strip_none(extract(content, fields))
|
||||
return content
|
||||
|
||||
@@ -11,12 +11,12 @@ class Comment(models.Model):
|
||||
'id', 'body', 'anonymous', 'anonymous_to_peers', 'course_id',
|
||||
'endorsed', 'parent_id', 'thread_id', 'username', 'votes', 'user_id',
|
||||
'closed', 'created_at', 'updated_at', 'depth', 'at_position_list',
|
||||
'type', 'commentable_id', 'abuse_flaggers'
|
||||
'type', 'commentable_id', 'abuse_flaggers', 'endorsement',
|
||||
]
|
||||
|
||||
updatable_fields = [
|
||||
'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'closed',
|
||||
'user_id', 'endorsed'
|
||||
'user_id', 'endorsed', 'endorsement_user_id',
|
||||
]
|
||||
|
||||
initializable_fields = updatable_fields
|
||||
|
||||
@@ -35,7 +35,7 @@ class Model(object):
|
||||
return self.__getattr__(name)
|
||||
|
||||
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)
|
||||
else:
|
||||
self.attributes[name] = value
|
||||
@@ -46,7 +46,7 @@ class Model(object):
|
||||
return self.attributes.get(key)
|
||||
|
||||
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))
|
||||
self.attributes.__setitem__(key, value)
|
||||
|
||||
|
||||
@@ -441,7 +441,8 @@ body.discussion {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
span {
|
||||
.timeago, .top-post-status {
|
||||
color: inherit;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,28 @@
|
||||
${"<% } else { %>"}
|
||||
<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>
|
||||
<div class="response-body">${"<%- body %>"}</div>
|
||||
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" role="button" aria-pressed="false" tabindex="0">
|
||||
|
||||
Reference in New Issue
Block a user