Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
182ae7ae
Commit
182ae7ae
authored
Jul 21, 2014
by
Greg Price
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add endorsement info to marked answers in forum
Co-authored-by: jsa <jsa@edx.org>
parent
10063a31
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
128 additions
and
21 deletions
+128
-21
common/static/coffee/spec/discussion/view/thread_response_show_view_spec.coffee
+63
-4
common/static/coffee/src/discussion/views/thread_response_show_view.coffee
+1
-1
lms/djangoapps/django_comment_client/base/views.py
+0
-0
lms/djangoapps/django_comment_client/forum/views.py
+6
-6
lms/djangoapps/django_comment_client/utils.py
+30
-4
lms/lib/comment_client/comment.py
+2
-2
lms/lib/comment_client/models.py
+2
-2
lms/static/sass/discussion/_discussion.scss
+2
-1
lms/templates/discussion/_underscore_templates.html
+22
-1
No files found.
common/static/coffee/spec/discussion/view/thread_response_show_view_spec.coffee
View file @
182ae7ae
...
...
@@ -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 "
)
common/static/coffee/src/discussion/views/thread_response_show_view.coffee
View file @
182ae7ae
...
...
@@ -29,7 +29,7 @@ if Backbone?
@
renderVote
()
@
renderAttrs
()
@
renderFlagged
()
@
$el
.
find
(
".posted-details"
).
timeago
()
@
$el
.
find
(
".posted-details
.timeago
"
).
timeago
()
@
convertMath
()
@
markAsStaff
()
@
...
...
lms/djangoapps/django_comment_client/base/views.py
View file @
182ae7ae
This diff is collapsed.
Click to expand it.
lms/djangoapps/django_comment_client/forum/views.py
View file @
182ae7ae
...
...
@@ -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'
],
})
...
...
lms/djangoapps/django_comment_client/utils.py
View file @
182ae7ae
...
...
@@ -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
lms/lib/comment_client/comment.py
View file @
182ae7ae
...
...
@@ -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
...
...
lms/lib/comment_client/models.py
View file @
182ae7ae
...
...
@@ -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
)
...
...
lms/static/sass/discussion/_discussion.scss
View file @
182ae7ae
...
...
@@ -441,7 +441,8 @@ body.discussion {
font-weight
:
700
;
}
span
{
.timeago
,
.top-post-status
{
color
:
inherit
;
font-style
:
italic
;
}
}
...
...
lms/templates/discussion/_underscore_templates.html
View file @
182ae7ae
...
...
@@ -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"
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment