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
ec482c0d
Commit
ec482c0d
authored
Jul 25, 2014
by
Greg Price
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Allow authors of forum questions to mark answers
Co-authored-by: jsa <jsa@edx.org>
parent
a3703fbf
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
186 additions
and
30 deletions
+186
-30
common/static/coffee/spec/discussion/content_spec.coffee
+38
-2
common/static/coffee/spec/discussion/view/thread_response_show_view_spec.coffee
+55
-1
common/static/coffee/src/discussion/content.coffee
+10
-1
common/static/coffee/src/discussion/utils.coffee
+3
-0
common/static/coffee/src/discussion/views/discussion_content_view.coffee
+0
-9
common/static/coffee/src/discussion/views/thread_response_show_view.coffee
+5
-9
lms/djangoapps/django_comment_client/base/tests.py
+52
-3
lms/djangoapps/django_comment_client/permissions.py
+16
-2
lms/djangoapps/django_comment_client/utils.py
+0
-1
lms/static/sass/discussion/_discussion.scss
+6
-0
lms/templates/discussion/_underscore_templates.html
+1
-2
No files found.
common/static/coffee/spec/discussion/content_spec.coffee
View file @
ec482c0d
...
...
@@ -41,11 +41,11 @@ describe 'All Content', ->
it
'can update info'
,
->
@
content
.
updateInfo
{
ability
:
'can_endorse'
,
ability
:
{
'can_edit'
:
true
}
,
voted
:
true
,
subscribed
:
true
}
expect
(
@
content
.
get
'ability'
).
toEqual
'can_endorse'
expect
(
@
content
.
get
'ability'
).
toEqual
{
'can_edit'
:
true
}
expect
(
@
content
.
get
'voted'
).
toEqual
true
expect
(
@
content
.
get
'subscribed'
).
toEqual
true
...
...
@@ -77,3 +77,39 @@ describe 'All Content', ->
myComments
=
new
Comments
myComments
.
add
@
comment1
expect
(
myComments
.
find
(
'123'
)).
toBe
@
comment1
it
'can be endorsed'
,
->
DiscussionUtil
.
loadRoles
(
{
"Moderator"
:
[
111
],
"Administrator"
:
[
222
],
"Community TA"
:
[
333
]}
)
@
discussionThread
=
new
Thread
({
id
:
1
,
thread_type
:
"discussion"
,
user_id
:
99
})
@
discussionResponse
=
new
Comment
({
id
:
1
,
thread
:
@
discussionThread
})
@
questionThread
=
new
Thread
({
id
:
1
,
thread_type
:
"question"
,
user_id
:
99
})
@
questionResponse
=
new
Comment
({
id
:
1
,
thread
:
@
questionThread
})
# mod
window
.
user
=
new
DiscussionUser
({
id
:
111
})
expect
(
@
discussionResponse
.
canBeEndorsed
()).
toBe
(
true
)
expect
(
@
questionResponse
.
canBeEndorsed
()).
toBe
(
true
)
# admin
window
.
user
=
new
DiscussionUser
({
id
:
222
})
expect
(
@
discussionResponse
.
canBeEndorsed
()).
toBe
(
true
)
expect
(
@
questionResponse
.
canBeEndorsed
()).
toBe
(
true
)
# TA
window
.
user
=
new
DiscussionUser
({
id
:
333
})
expect
(
@
discussionResponse
.
canBeEndorsed
()).
toBe
(
true
)
expect
(
@
questionResponse
.
canBeEndorsed
()).
toBe
(
true
)
# thread author
window
.
user
=
new
DiscussionUser
({
id
:
99
})
expect
(
@
discussionResponse
.
canBeEndorsed
()).
toBe
(
false
)
expect
(
@
questionResponse
.
canBeEndorsed
()).
toBe
(
true
)
# anyone else
window
.
user
=
new
DiscussionUser
({
id
:
999
})
expect
(
@
discussionResponse
.
canBeEndorsed
()).
toBe
(
false
)
expect
(
@
questionResponse
.
canBeEndorsed
()).
toBe
(
false
)
common/static/coffee/spec/discussion/view/thread_response_show_view_spec.coffee
View file @
ec482c0d
...
...
@@ -41,6 +41,7 @@ describe "ThreadResponseShowView", ->
course_id
:
"TestOrg/TestCourse/TestRun"
,
body
:
"this is a comment"
,
created_at
:
"2013-04-03T20:08:39Z"
,
endorsed
:
false
,
abuse_flaggers
:
[],
votes
:
{
up_count
:
"42"
}
}
...
...
@@ -99,8 +100,8 @@ describe "ThreadResponseShowView", ->
expect
(
@
view
.
$
(
".posted-details"
).
text
()).
not
.
toMatch
(
" by "
)
it
"re-renders correctly when endorsement changes"
,
->
DiscussionUtil
.
loadRoles
({
"Moderator"
:
[
parseInt
(
window
.
user
.
id
)]})
@
thread
.
set
(
"thread_type"
,
"question"
)
@
comment
.
updateInfo
({
"ability"
:
{
"can_endorse"
:
true
}})
expect
(
@
view
.
$
(
".posted-details"
).
text
()).
not
.
toMatch
(
"marked as answer"
)
@
view
.
$
(
".action-endorse"
).
click
()
expect
(
@
view
.
$
(
".posted-details"
).
text
()).
toMatch
(
...
...
@@ -108,3 +109,56 @@ describe "ThreadResponseShowView", ->
)
@
view
.
$
(
".action-endorse"
).
click
()
expect
(
@
view
.
$
(
".posted-details"
).
text
()).
not
.
toMatch
(
"marked as answer"
)
it
"allows a moderator to mark an answer in a question thread"
,
->
DiscussionUtil
.
loadRoles
({
"Moderator"
:
parseInt
(
window
.
user
.
id
)})
@
thread
.
set
({
"thread_type"
:
"question"
,
"user_id"
:
(
parseInt
(
window
.
user
.
id
)
+
1
).
toString
()
})
@
view
.
render
()
endorseButton
=
@
view
.
$
(
".action-endorse"
)
expect
(
endorseButton
.
length
).
toEqual
(
1
)
expect
(
endorseButton
).
not
.
toHaveCss
({
"display"
:
"none"
})
expect
(
endorseButton
).
toHaveClass
(
"is-clickable"
)
endorseButton
.
click
()
expect
(
endorseButton
).
toHaveClass
(
"is-endorsed"
)
it
"allows the author of a question thread to mark an answer"
,
->
@
thread
.
set
({
"thread_type"
:
"question"
,
"user_id"
:
window
.
user
.
id
})
@
view
.
render
()
endorseButton
=
@
view
.
$
(
".action-endorse"
)
expect
(
endorseButton
.
length
).
toEqual
(
1
)
expect
(
endorseButton
).
not
.
toHaveCss
({
"display"
:
"none"
})
expect
(
endorseButton
).
toHaveClass
(
"is-clickable"
)
endorseButton
.
click
()
expect
(
endorseButton
).
toHaveClass
(
"is-endorsed"
)
it
"does not allow the author of a discussion thread to endorse"
,
->
@
thread
.
set
({
"thread_type"
:
"discussion"
,
"user_id"
:
window
.
user
.
id
})
@
view
.
render
()
endorseButton
=
@
view
.
$
(
".action-endorse"
)
expect
(
endorseButton
.
length
).
toEqual
(
1
)
expect
(
endorseButton
).
toHaveCss
({
"display"
:
"none"
})
expect
(
endorseButton
).
not
.
toHaveClass
(
"is-clickable"
)
endorseButton
.
click
()
expect
(
endorseButton
).
not
.
toHaveClass
(
"is-endorsed"
)
it
"does not allow a student who is not the author of a question thread to mark an answer"
,
->
@
thread
.
set
({
"thread_type"
:
"question"
,
"user_id"
:
(
parseInt
(
window
.
user
.
id
)
+
1
).
toString
()
})
@
view
.
render
()
endorseButton
=
@
view
.
$
(
".action-endorse"
)
expect
(
endorseButton
.
length
).
toEqual
(
1
)
expect
(
endorseButton
).
toHaveCss
({
"display"
:
"none"
})
expect
(
endorseButton
).
not
.
toHaveClass
(
"is-clickable"
)
endorseButton
.
click
()
expect
(
endorseButton
).
not
.
toHaveClass
(
"is-endorsed"
)
common/static/coffee/src/discussion/content.coffee
View file @
ec482c0d
...
...
@@ -9,7 +9,6 @@ if Backbone?
actions
:
editable
:
'.admin-edit'
can_reply
:
'.discussion-reply'
can_endorse
:
'.admin-endorse'
can_delete
:
'.admin-delete'
can_openclose
:
'.admin-openclose'
...
...
@@ -21,6 +20,9 @@ if Backbone?
can
:
(
action
)
->
(
@
get
(
'ability'
)
||
{})[
action
]
# Default implementation
canBeEndorsed
:
->
false
updateInfo
:
(
info
)
->
if
info
@
set
(
'ability'
,
info
.
ability
)
...
...
@@ -187,6 +189,13 @@ if Backbone?
count
+=
comment
.
getCommentsCount
()
+
1
count
canBeEndorsed
:
=>
user_id
=
window
.
user
.
get
(
"id"
)
user_id
&&
(
DiscussionUtil
.
isPrivilegedUser
(
user_id
)
||
(
@
get
(
'thread'
).
get
(
'thread_type'
)
==
'question'
&&
@
get
(
'thread'
).
get
(
'user_id'
)
==
user_id
)
)
class
@
Comments
extends
Backbone
.
Collection
model
:
Comment
...
...
common/static/coffee/src/discussion/utils.coffee
View file @
ec482c0d
...
...
@@ -41,6 +41,9 @@ class @DiscussionUtil
ta
=
_
.
union
(
@
roleIds
[
'Community TA'
])
_
.
include
(
ta
,
parseInt
(
user_id
))
@
isPrivilegedUser
:
(
user_id
)
->
@
isStaff
(
user_id
)
||
@
isTA
(
user_id
)
@
bulkUpdateContentInfo
:
(
infos
)
->
for
id
,
info
of
infos
Content
.
getContent
(
id
).
updateInfo
(
info
)
...
...
common/static/coffee/src/discussion/views/discussion_content_view.coffee
View file @
ec482c0d
...
...
@@ -46,15 +46,6 @@ if Backbone?
can_delete
:
enable
:
->
@
$
(
".action-delete"
).
closest
(
"li"
).
show
()
disable
:
->
@
$
(
".action-delete"
).
closest
(
"li"
).
hide
()
can_endorse
:
enable
:
->
@
$
(
".action-endorse"
).
show
().
css
(
"cursor"
,
"auto"
)
disable
:
->
@
$
(
".action-endorse"
).
css
(
"cursor"
,
"default"
)
if
not
@
model
.
get
(
'endorsed'
)
@
$
(
".action-endorse"
).
hide
()
else
@
$
(
".action-endorse"
).
show
()
can_openclose
:
enable
:
->
@
$
(
".action-openclose"
).
closest
(
"li"
).
show
()
disable
:
->
@
$
(
".action-openclose"
).
closest
(
"li"
).
hide
()
...
...
common/static/coffee/src/discussion/views/thread_response_show_view.coffee
View file @
ec482c0d
...
...
@@ -14,14 +14,10 @@ if Backbone?
attrRenderer
:
$
.
extend
({},
DiscussionContentView
.
prototype
.
attrRenderer
,
{
endorsed
:
(
endorsed
)
->
if
endorsed
@
$
(
".action-endorse"
).
show
().
addClass
(
"is-endorsed"
)
else
if
@
model
.
get
(
'ability'
)
?
.
can_endorse
@
$
(
".action-endorse"
).
show
()
else
@
$
(
".action-endorse"
).
hide
()
@
$
(
".action-endorse"
).
removeClass
(
"is-endorsed"
)
$endorseButton
=
@
$
(
".action-endorse"
)
$endorseButton
.
toggleClass
(
"is-clickable"
,
@
model
.
canBeEndorsed
())
$endorseButton
.
toggleClass
(
"is-endorsed"
,
endorsed
)
$endorseButton
.
toggle
(
endorsed
||
@
model
.
canBeEndorsed
())
})
$
:
(
selector
)
->
...
...
@@ -67,7 +63,7 @@ if Backbone?
toggleEndorse
:
(
event
)
->
event
.
preventDefault
()
if
not
@
model
.
can
(
'can_endorse'
)
if
not
@
model
.
can
BeEndorsed
(
)
return
$elem
=
$
(
event
.
target
)
url
=
@
model
.
urlFor
(
'endorse'
)
...
...
lms/djangoapps/django_comment_client/base/tests.py
View file @
ec482c0d
...
...
@@ -6,7 +6,7 @@ from django.test.utils import override_settings
from
django.contrib.auth.models
import
User
from
django.core.management
import
call_command
from
django.core.urlresolvers
import
reverse
from
mock
import
patch
,
ANY
from
mock
import
patch
,
ANY
,
Mock
from
nose.tools
import
assert_true
,
assert_equal
# pylint: disable=E0611
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
...
...
@@ -26,9 +26,11 @@ CS_PREFIX = "http://localhost:4567/api/v1"
class
MockRequestSetupMixin
(
object
):
def
_create_repsonse_mock
(
self
,
data
):
return
Mock
(
text
=
json
.
dumps
(
data
),
json
=
Mock
(
return_value
=
data
))
\
def
_set_mock_request_data
(
self
,
mock_request
,
data
):
mock_request
.
return_value
.
text
=
json
.
dumps
(
data
)
mock_request
.
return_value
.
json
.
return_value
=
data
mock_request
.
return_value
=
self
.
_create_repsonse_mock
(
data
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MIXED_MODULESTORE
)
...
...
@@ -620,6 +622,53 @@ class ViewPermissionsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSet
)
self
.
assertEqual
(
response
.
status_code
,
200
)
def
_set_mock_request_thread_and_comment
(
self
,
mock_request
,
thread_data
,
comment_data
):
def
handle_request
(
*
args
,
**
kwargs
):
url
=
args
[
1
]
if
"/threads/"
in
url
:
return
self
.
_create_repsonse_mock
(
thread_data
)
elif
"/comments/"
in
url
:
return
self
.
_create_repsonse_mock
(
comment_data
)
else
:
raise
ArgumentError
(
"Bad url to mock request"
)
mock_request
.
side_effect
=
handle_request
def
test_endorse_response_as_staff
(
self
,
mock_request
):
self
.
_set_mock_request_thread_and_comment
(
mock_request
,
{
"type"
:
"thread"
,
"thread_type"
:
"question"
,
"user_id"
:
str
(
self
.
student
.
id
)},
{
"type"
:
"comment"
,
"thread_id"
:
"dummy"
}
)
self
.
client
.
login
(
username
=
self
.
moderator
.
username
,
password
=
self
.
password
)
response
=
self
.
client
.
post
(
reverse
(
"endorse_comment"
,
kwargs
=
{
"course_id"
:
self
.
course
.
id
.
to_deprecated_string
(),
"comment_id"
:
"dummy"
})
)
self
.
assertEqual
(
response
.
status_code
,
200
)
def
test_endorse_response_as_student
(
self
,
mock_request
):
self
.
_set_mock_request_thread_and_comment
(
mock_request
,
{
"type"
:
"thread"
,
"thread_type"
:
"question"
,
"user_id"
:
str
(
self
.
moderator
.
id
)},
{
"type"
:
"comment"
,
"thread_id"
:
"dummy"
}
)
self
.
client
.
login
(
username
=
self
.
student
.
username
,
password
=
self
.
password
)
response
=
self
.
client
.
post
(
reverse
(
"endorse_comment"
,
kwargs
=
{
"course_id"
:
self
.
course
.
id
.
to_deprecated_string
(),
"comment_id"
:
"dummy"
})
)
self
.
assertEqual
(
response
.
status_code
,
401
)
def
test_endorse_response_as_student_question_author
(
self
,
mock_request
):
self
.
_set_mock_request_thread_and_comment
(
mock_request
,
{
"type"
:
"thread"
,
"thread_type"
:
"question"
,
"user_id"
:
str
(
self
.
student
.
id
)},
{
"type"
:
"comment"
,
"thread_id"
:
"dummy"
}
)
self
.
client
.
login
(
username
=
self
.
student
.
username
,
password
=
self
.
password
)
response
=
self
.
client
.
post
(
reverse
(
"endorse_comment"
,
kwargs
=
{
"course_id"
:
self
.
course
.
id
.
to_deprecated_string
(),
"comment_id"
:
"dummy"
})
)
self
.
assertEqual
(
response
.
status_code
,
200
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MIXED_MODULESTORE
)
class
CreateThreadUnicodeTestCase
(
ModuleStoreTestCase
,
UnicodeTestMixin
,
MockRequestSetupMixin
):
...
...
lms/djangoapps/django_comment_client/permissions.py
View file @
ec482c0d
...
...
@@ -5,6 +5,7 @@ Module for checking permissions with the comment_client backend
import
logging
from
types
import
NoneType
from
django.core
import
cache
from
lms.lib.comment_client
import
Thread
from
opaque_keys.edx.keys
import
CourseKey
CACHE
=
cache
.
get_cache
(
'default'
)
...
...
@@ -34,7 +35,7 @@ def has_permission(user, permission, course_id=None):
return
False
CONDITIONS
=
[
'is_open'
,
'is_author'
]
CONDITIONS
=
[
'is_open'
,
'is_author'
,
'is_question_author'
]
def
_check_condition
(
user
,
condition
,
content
):
...
...
@@ -50,9 +51,22 @@ def _check_condition(user, condition, content):
except
KeyError
:
return
False
def
check_question_author
(
user
,
content
):
if
not
content
:
return
False
try
:
if
content
[
"type"
]
==
"thread"
:
return
content
[
"thread_type"
]
==
"question"
and
content
[
"user_id"
]
==
str
(
user
.
id
)
else
:
# N.B. This will trigger a comments service query
return
check_question_author
(
user
,
Thread
(
id
=
content
[
"thread_id"
])
.
to_dict
())
except
KeyError
:
return
False
handlers
=
{
'is_open'
:
check_open
,
'is_author'
:
check_author
,
'is_question_author'
:
check_question_author
,
}
return
handlers
[
condition
](
user
,
content
)
...
...
@@ -85,7 +99,7 @@ VIEW_PERMISSIONS = {
'create_comment'
:
[[
"create_comment"
,
"is_open"
]],
'delete_thread'
:
[
'delete_thread'
,
[
'update_thread'
,
'is_author'
]],
'update_comment'
:
[
'edit_content'
,
[
'update_comment'
,
'is_open'
,
'is_author'
]],
'endorse_comment'
:
[
'endorse_comment'
],
'endorse_comment'
:
[
'endorse_comment'
,
'is_question_author'
],
'openclose_thread'
:
[
'openclose_thread'
],
'create_sub_comment'
:
[[
'create_sub_comment'
,
'is_open'
]],
'delete_comment'
:
[
'delete_comment'
,
[
'update_comment'
,
'is_open'
,
'is_author'
]],
...
...
lms/djangoapps/django_comment_client/utils.py
View file @
ec482c0d
...
...
@@ -258,7 +258,6 @@ def get_ability(course_id, content, user):
return
{
'editable'
:
check_permissions_by_view
(
user
,
course_id
,
content
,
"update_thread"
if
content
[
'type'
]
==
'thread'
else
"update_comment"
),
'can_reply'
:
check_permissions_by_view
(
user
,
course_id
,
content
,
"create_comment"
if
content
[
'type'
]
==
'thread'
else
"create_sub_comment"
),
'can_endorse'
:
check_permissions_by_view
(
user
,
course_id
,
content
,
"endorse_comment"
)
if
content
[
'type'
]
==
'comment'
else
False
,
'can_delete'
:
check_permissions_by_view
(
user
,
course_id
,
content
,
"delete_thread"
if
content
[
'type'
]
==
'thread'
else
"delete_comment"
),
'can_openclose'
:
check_permissions_by_view
(
user
,
course_id
,
content
,
"openclose_thread"
)
if
content
[
'type'
]
==
'thread'
else
False
,
'can_vote'
:
check_permissions_by_view
(
user
,
course_id
,
content
,
"vote_for_thread"
if
content
[
'type'
]
==
'thread'
else
"vote_for_comment"
),
...
...
lms/static/sass/discussion/_discussion.scss
View file @
ec482c0d
...
...
@@ -647,6 +647,11 @@ body.discussion {
border
:
1px
solid
#a0a0a0
;
@include
linear-gradient
(
top
,
$white
35%
,
$gray-l4
);
box-shadow
:
0
1px
1px
$shadow-l1
;
cursor
:
default
;
&
.is-clickable
{
cursor
:
auto
;
}
.check-icon
{
display
:
block
;
...
...
@@ -654,6 +659,7 @@ body.discussion {
height
:
12px
;
margin
:
8px
auto
;
background
:
url(../images/endorse-icon.png)
no-repeat
;
pointer-events
:
none
;
}
&
.mark-answer
.check-icon
{
...
...
lms/templates/discussion/_underscore_templates.html
View file @
ec482c0d
...
...
@@ -162,10 +162,9 @@
<
a
href
=
"javascript:void(0)"
class
=
"endorse-btn action-endorse ${"
<%=
thread
.
get
(
'thread_type'
)
==
'question'
?
'mark-answer'
:
''
%>
"}"
style
=
"cursor: default; display: none;"
data
-
tooltip
=
"${tooltip_expr}"
>
<
span
class
=
"check-icon"
style
=
"pointer-events: none; "
><
/span
>
<
span
class
=
"check-icon"
><
/span
>
<
/a
>
$
{
"<% if (obj.username) { %>"
}
<
a
href
=
"${'<%- user_url %>'}"
class
=
"posted-by"
>
$
{
'<%- username %>'
}
<
/a
>
...
...
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