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
94c1bf49
Commit
94c1bf49
authored
Jun 11, 2015
by
Greg Price
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #8437 from edx/gprice/discussion-api-edit-comment
Add comment editing to discussion API
parents
26f1ecb6
895731f5
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
480 additions
and
37 deletions
+480
-37
lms/djangoapps/discussion_api/api.py
+65
-3
lms/djangoapps/discussion_api/serializers.py
+19
-8
lms/djangoapps/discussion_api/tests/test_api.py
+168
-0
lms/djangoapps/discussion_api/tests/test_serializers.py
+76
-6
lms/djangoapps/discussion_api/tests/test_views.py
+87
-1
lms/djangoapps/discussion_api/tests/utils.py
+45
-17
lms/djangoapps/discussion_api/views.py
+20
-2
No files found.
lms/djangoapps/discussion_api/api.py
View file @
94c1bf49
...
...
@@ -26,6 +26,7 @@ from django_comment_client.base.views import (
track_forum_event
,
)
from
django_comment_client.utils
import
get_accessible_discussion_modules
from
lms.lib.comment_client.comment
import
Comment
from
lms.lib.comment_client.thread
import
Thread
from
lms.lib.comment_client.utils
import
CommentClientRequestError
from
openedx.core.djangoapps.course_groups.cohorts
import
get_cohort_id
,
is_commentable_cohorted
...
...
@@ -74,16 +75,36 @@ def _get_thread_and_context(request, thread_id, retrieve_kwargs=None):
raise
Http404
def
_
is_user_author_or_privileged
(
cc_thread
,
context
):
def
_
get_comment_and_context
(
request
,
comment_id
):
"""
Check if the user is the author of a thread or a privileged user.
Retrieve the given comment and build a serializer context for it, returning
both. This function also enforces access control for the comment (checking
both the user's access to the course and to the comment's thread's cohort if
applicable). Raises Http404 if the comment does not exist or the user cannot
access it.
"""
try
:
cc_comment
=
Comment
(
id
=
comment_id
)
.
retrieve
()
_
,
context
=
_get_thread_and_context
(
request
,
cc_comment
[
"thread_id"
],
cc_comment
[
"parent_id"
]
)
return
cc_comment
,
context
except
CommentClientRequestError
:
raise
Http404
def
_is_user_author_or_privileged
(
cc_content
,
context
):
"""
Check if the user is the author of a content object or a privileged user.
Returns:
Boolean
"""
return
(
context
[
"is_requester_privileged"
]
or
context
[
"cc_requester"
][
"id"
]
==
cc_
thread
[
"user_id"
]
context
[
"cc_requester"
][
"id"
]
==
cc_
content
[
"user_id"
]
)
...
...
@@ -442,6 +463,47 @@ def update_thread(request, thread_id, update_data):
return
api_thread
def
update_comment
(
request
,
comment_id
,
update_data
):
"""
Update a comment.
Parameters:
request: The django request object used for build_absolute_uri and
determining the requesting user.
comment_id: The id for the comment to update.
update_data: The data to update in the comment.
Returns:
The updated comment; see discussion_api.views.CommentViewSet for more
detail.
Raises:
Http404: if the comment does not exist or is not accessible to the
requesting user
PermissionDenied: if the comment is accessible to but not editable by
the requesting user
ValidationError: if there is an error applying the update (e.g. raw_body
is empty or thread_id is included)
"""
cc_comment
,
context
=
_get_comment_and_context
(
request
,
comment_id
)
if
not
_is_user_author_or_privileged
(
cc_comment
,
context
):
raise
PermissionDenied
()
serializer
=
CommentSerializer
(
cc_comment
,
data
=
update_data
,
partial
=
True
,
context
=
context
)
if
not
serializer
.
is_valid
():
raise
ValidationError
(
serializer
.
errors
)
# Only save comment object if the comment is actually modified
if
update_data
:
serializer
.
save
()
return
serializer
.
data
def
delete_thread
(
request
,
thread_id
):
"""
Delete a thread.
...
...
lms/djangoapps/discussion_api/serializers.py
View file @
94c1bf49
...
...
@@ -69,12 +69,23 @@ class _ContentSerializer(serializers.Serializer):
voted
=
serializers
.
SerializerMethodField
(
"get_voted"
)
vote_count
=
serializers
.
SerializerMethodField
(
"get_vote_count"
)
non_updatable_fields
=
()
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
_ContentSerializer
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
# id is an invalid class attribute name, so we must declare a different
# name above and modify it here
self
.
fields
[
"id"
]
=
self
.
fields
.
pop
(
"id_"
)
for
field
in
self
.
non_updatable_fields
:
setattr
(
self
,
"validate_{}"
.
format
(
field
),
self
.
_validate_non_updatable
)
def
_validate_non_updatable
(
self
,
attrs
,
_source
):
"""Ensure that a field is not edited in an update operation."""
if
self
.
object
:
raise
ValidationError
(
"This field is not allowed in an update."
)
return
attrs
def
_is_user_privileged
(
self
,
user_id
):
"""
Returns a boolean indicating whether the given user_id identifies a
...
...
@@ -156,6 +167,8 @@ class ThreadSerializer(_ContentSerializer):
endorsed_comment_list_url
=
serializers
.
SerializerMethodField
(
"get_endorsed_comment_list_url"
)
non_endorsed_comment_list_url
=
serializers
.
SerializerMethodField
(
"get_non_endorsed_comment_list_url"
)
non_updatable_fields
=
(
"course_id"
,)
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
ThreadSerializer
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
# type is an invalid class attribute name, so we must declare a
...
...
@@ -199,12 +212,6 @@ class ThreadSerializer(_ContentSerializer):
"""Returns the URL to retrieve the thread's non-endorsed comments."""
return
self
.
get_comment_list_url
(
obj
,
endorsed
=
False
)
def
validate_course_id
(
self
,
attrs
,
_source
):
"""Ensure that course_id is not edited in an update operation."""
if
self
.
object
:
raise
ValidationError
(
"This field is not allowed in an update."
)
return
attrs
def
restore_object
(
self
,
attrs
,
instance
=
None
):
if
instance
:
for
key
,
val
in
attrs
.
items
():
...
...
@@ -230,6 +237,8 @@ class CommentSerializer(_ContentSerializer):
endorsed_at
=
serializers
.
SerializerMethodField
(
"get_endorsed_at"
)
children
=
serializers
.
SerializerMethodField
(
"get_children"
)
non_updatable_fields
=
(
"thread_id"
,
"parent_id"
)
def
get_endorsed_by
(
self
,
obj
):
"""
Returns the username of the endorsing user, if the information is
...
...
@@ -288,8 +297,10 @@ class CommentSerializer(_ContentSerializer):
return
attrs
def
restore_object
(
self
,
attrs
,
instance
=
None
):
if
instance
:
# pragma: no cover
raise
ValueError
(
"CommentSerializer cannot be used for updates."
)
if
instance
:
for
key
,
val
in
attrs
.
items
():
instance
[
key
]
=
val
return
instance
return
Comment
(
course_id
=
self
.
context
[
"thread"
][
"course_id"
],
user_id
=
self
.
context
[
"cc_requester"
][
"id"
],
...
...
lms/djangoapps/discussion_api/tests/test_api.py
View file @
94c1bf49
...
...
@@ -27,6 +27,7 @@ from discussion_api.api import (
get_comment_list
,
get_course_topics
,
get_thread_list
,
update_comment
,
update_thread
,
)
from
discussion_api.tests.utils
import
(
...
...
@@ -1638,6 +1639,173 @@ class UpdateThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestC
@ddt.ddt
class
UpdateCommentTest
(
CommentsServiceMockMixin
,
UrlResetMixin
,
ModuleStoreTestCase
):
"""Tests for update_comment"""
@mock.patch.dict
(
"django.conf.settings.FEATURES"
,
{
"ENABLE_DISCUSSION_SERVICE"
:
True
})
def
setUp
(
self
):
super
(
UpdateCommentTest
,
self
)
.
setUp
()
httpretty
.
reset
()
httpretty
.
enable
()
self
.
addCleanup
(
httpretty
.
disable
)
self
.
user
=
UserFactory
.
create
()
self
.
register_get_user_response
(
self
.
user
)
self
.
request
=
RequestFactory
()
.
get
(
"/test_path"
)
self
.
request
.
user
=
self
.
user
self
.
course
=
CourseFactory
.
create
()
CourseEnrollmentFactory
.
create
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
def
register_comment
(
self
,
overrides
=
None
,
thread_overrides
=
None
):
"""
Make a comment with appropriate data overridden by the overrides
parameter and register mock responses for both GET and PUT on its
endpoint. Also mock GET for the related thread with thread_overrides.
"""
cs_thread_data
=
make_minimal_cs_thread
({
"id"
:
"test_thread"
,
"course_id"
:
unicode
(
self
.
course
.
id
)
})
cs_thread_data
.
update
(
thread_overrides
or
{})
self
.
register_get_thread_response
(
cs_thread_data
)
cs_comment_data
=
make_minimal_cs_comment
({
"id"
:
"test_comment"
,
"course_id"
:
cs_thread_data
[
"course_id"
],
"thread_id"
:
cs_thread_data
[
"id"
],
"username"
:
self
.
user
.
username
,
"user_id"
:
str
(
self
.
user
.
id
),
"created_at"
:
"2015-06-03T00:00:00Z"
,
"updated_at"
:
"2015-06-03T00:00:00Z"
,
"body"
:
"Original body"
,
})
cs_comment_data
.
update
(
overrides
or
{})
self
.
register_get_comment_response
(
cs_comment_data
)
self
.
register_put_comment_response
(
cs_comment_data
)
def
test_empty
(
self
):
"""Check that an empty update does not make any modifying requests."""
self
.
register_comment
()
update_comment
(
self
.
request
,
"test_comment"
,
{})
for
request
in
httpretty
.
httpretty
.
latest_requests
:
self
.
assertEqual
(
request
.
method
,
"GET"
)
def
test_basic
(
self
):
self
.
register_comment
()
actual
=
update_comment
(
self
.
request
,
"test_comment"
,
{
"raw_body"
:
"Edited body"
})
expected
=
{
"id"
:
"test_comment"
,
"thread_id"
:
"test_thread"
,
"parent_id"
:
None
,
# TODO: we can't get this without retrieving from the thread :-(
"author"
:
self
.
user
.
username
,
"author_label"
:
None
,
"created_at"
:
"2015-06-03T00:00:00Z"
,
"updated_at"
:
"2015-06-03T00:00:00Z"
,
"raw_body"
:
"Edited body"
,
"endorsed"
:
False
,
"endorsed_by"
:
None
,
"endorsed_by_label"
:
None
,
"endorsed_at"
:
None
,
"abuse_flagged"
:
False
,
"voted"
:
False
,
"vote_count"
:
0
,
"children"
:
[],
}
self
.
assertEqual
(
actual
,
expected
)
self
.
assertEqual
(
httpretty
.
last_request
()
.
parsed_body
,
{
"body"
:
[
"Edited body"
],
"course_id"
:
[
unicode
(
self
.
course
.
id
)],
"user_id"
:
[
str
(
self
.
user
.
id
)],
"anonymous"
:
[
"False"
],
"anonymous_to_peers"
:
[
"False"
],
"endorsed"
:
[
"False"
],
}
)
def
test_nonexistent_comment
(
self
):
self
.
register_get_comment_error_response
(
"test_comment"
,
404
)
with
self
.
assertRaises
(
Http404
):
update_comment
(
self
.
request
,
"test_comment"
,
{})
def
test_nonexistent_course
(
self
):
self
.
register_comment
(
thread_overrides
=
{
"course_id"
:
"non/existent/course"
})
with
self
.
assertRaises
(
Http404
):
update_comment
(
self
.
request
,
"test_comment"
,
{})
def
test_unenrolled
(
self
):
self
.
register_comment
()
self
.
request
.
user
=
UserFactory
.
create
()
with
self
.
assertRaises
(
Http404
):
update_comment
(
self
.
request
,
"test_comment"
,
{})
def
test_discussions_disabled
(
self
):
_remove_discussion_tab
(
self
.
course
,
self
.
user
.
id
)
self
.
register_comment
()
with
self
.
assertRaises
(
Http404
):
update_comment
(
self
.
request
,
"test_comment"
,
{})
@ddt.data
(
*
itertools
.
product
(
[
FORUM_ROLE_ADMINISTRATOR
,
FORUM_ROLE_MODERATOR
,
FORUM_ROLE_COMMUNITY_TA
,
FORUM_ROLE_STUDENT
,
],
[
True
,
False
],
[
"no_group"
,
"match_group"
,
"different_group"
],
)
)
@ddt.unpack
def
test_group_access
(
self
,
role_name
,
course_is_cohorted
,
thread_group_state
):
cohort_course
=
CourseFactory
.
create
(
cohort_config
=
{
"cohorted"
:
course_is_cohorted
})
CourseEnrollmentFactory
.
create
(
user
=
self
.
user
,
course_id
=
cohort_course
.
id
)
cohort
=
CohortFactory
.
create
(
course_id
=
cohort_course
.
id
,
users
=
[
self
.
user
])
role
=
Role
.
objects
.
create
(
name
=
role_name
,
course_id
=
cohort_course
.
id
)
role
.
users
=
[
self
.
user
]
self
.
register_get_thread_response
(
make_minimal_cs_thread
())
self
.
register_comment
(
{
"thread_id"
:
"test_thread"
},
thread_overrides
=
{
"id"
:
"test_thread"
,
"course_id"
:
unicode
(
cohort_course
.
id
),
"group_id"
:
(
None
if
thread_group_state
==
"no_group"
else
cohort
.
id
if
thread_group_state
==
"match_group"
else
cohort
.
id
+
1
),
}
)
expected_error
=
(
role_name
==
FORUM_ROLE_STUDENT
and
course_is_cohorted
and
thread_group_state
==
"different_group"
)
try
:
update_comment
(
self
.
request
,
"test_comment"
,
{})
self
.
assertFalse
(
expected_error
)
except
Http404
:
self
.
assertTrue
(
expected_error
)
@ddt.data
(
FORUM_ROLE_ADMINISTRATOR
,
FORUM_ROLE_MODERATOR
,
FORUM_ROLE_COMMUNITY_TA
,
FORUM_ROLE_STUDENT
,
)
def
test_role_access
(
self
,
role_name
):
role
=
Role
.
objects
.
create
(
name
=
role_name
,
course_id
=
self
.
course
.
id
)
role
.
users
=
[
self
.
user
]
self
.
register_comment
({
"user_id"
:
str
(
self
.
user
.
id
+
1
)})
expected_error
=
role_name
==
FORUM_ROLE_STUDENT
try
:
update_comment
(
self
.
request
,
"test_comment"
,
{
"raw_body"
:
"edited"
})
self
.
assertFalse
(
expected_error
)
except
PermissionDenied
:
self
.
assertTrue
(
expected_error
)
@ddt.ddt
class
DeleteThreadTest
(
CommentsServiceMockMixin
,
UrlResetMixin
,
ModuleStoreTestCase
):
"""Tests for delete_thread"""
@mock.patch.dict
(
"django.conf.settings.FEATURES"
,
{
"ENABLE_DISCUSSION_SERVICE"
:
True
})
...
...
lms/djangoapps/discussion_api/tests/test_serializers.py
View file @
94c1bf49
...
...
@@ -23,6 +23,7 @@ from django_comment_common.models import (
FORUM_ROLE_STUDENT
,
Role
,
)
from
lms.lib.comment_client.comment
import
Comment
from
lms.lib.comment_client.thread
import
Thread
from
student.tests.factories
import
UserFactory
from
util.testing
import
UrlResetMixin
...
...
@@ -553,8 +554,15 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, ModuleStore
"thread_id"
:
"test_thread"
,
"raw_body"
:
"Test body"
,
}
self
.
existing_comment
=
Comment
(
**
make_minimal_cs_comment
({
"id"
:
"existing_comment"
,
"thread_id"
:
"existing_thread"
,
"body"
:
"Original body"
,
"user_id"
:
str
(
self
.
user
.
id
),
"course_id"
:
unicode
(
self
.
course
.
id
),
}))
def
save_and_reserialize
(
self
,
data
):
def
save_and_reserialize
(
self
,
data
,
instance
=
None
):
"""
Create a serializer with the given data, ensure that it is valid, save
the result, and return the full comment data from the serializer.
...
...
@@ -564,13 +572,18 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, ModuleStore
self
.
request
,
make_minimal_cs_thread
({
"course_id"
:
unicode
(
self
.
course
.
id
)})
)
serializer
=
CommentSerializer
(
data
=
data
,
context
=
context
)
serializer
=
CommentSerializer
(
instance
,
data
=
data
,
partial
=
(
instance
is
not
None
),
context
=
context
)
self
.
assertTrue
(
serializer
.
is_valid
())
serializer
.
save
()
return
serializer
.
data
@ddt.data
(
None
,
"test_parent"
)
def
test_success
(
self
,
parent_id
):
def
test_
create_
success
(
self
,
parent_id
):
data
=
self
.
minimal_data
.
copy
()
if
parent_id
:
data
[
"parent_id"
]
=
parent_id
...
...
@@ -597,7 +610,7 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, ModuleStore
self
.
assertEqual
(
saved
[
"id"
],
"test_comment"
)
self
.
assertEqual
(
saved
[
"parent_id"
],
parent_id
)
def
test_parent_id_nonexistent
(
self
):
def
test_
create_
parent_id_nonexistent
(
self
):
self
.
register_get_comment_error_response
(
"bad_parent"
,
404
)
data
=
self
.
minimal_data
.
copy
()
data
[
"parent_id"
]
=
"bad_parent"
...
...
@@ -613,7 +626,7 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, ModuleStore
}
)
def
test_parent_id_wrong_thread
(
self
):
def
test_
create_
parent_id_wrong_thread
(
self
):
self
.
register_get_comment_response
({
"thread_id"
:
"different_thread"
,
"id"
:
"test_parent"
})
data
=
self
.
minimal_data
.
copy
()
data
[
"parent_id"
]
=
"test_parent"
...
...
@@ -629,7 +642,7 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, ModuleStore
}
)
def
test_missing_field
(
self
):
def
test_
create_
missing_field
(
self
):
for
field
in
self
.
minimal_data
:
data
=
self
.
minimal_data
.
copy
()
data
.
pop
(
field
)
...
...
@@ -642,3 +655,60 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, ModuleStore
serializer
.
errors
,
{
field
:
[
"This field is required."
]}
)
def
test_update_empty
(
self
):
self
.
register_put_comment_response
(
self
.
existing_comment
.
attributes
)
self
.
save_and_reserialize
({},
instance
=
self
.
existing_comment
)
self
.
assertEqual
(
httpretty
.
last_request
()
.
parsed_body
,
{
"body"
:
[
"Original body"
],
"course_id"
:
[
unicode
(
self
.
course
.
id
)],
"user_id"
:
[
str
(
self
.
user
.
id
)],
"anonymous"
:
[
"False"
],
"anonymous_to_peers"
:
[
"False"
],
"endorsed"
:
[
"False"
],
}
)
def
test_update_all
(
self
):
self
.
register_put_comment_response
(
self
.
existing_comment
.
attributes
)
data
=
{
"raw_body"
:
"Edited body"
}
saved
=
self
.
save_and_reserialize
(
data
,
instance
=
self
.
existing_comment
)
self
.
assertEqual
(
httpretty
.
last_request
()
.
parsed_body
,
{
"body"
:
[
"Edited body"
],
"course_id"
:
[
unicode
(
self
.
course
.
id
)],
"user_id"
:
[
str
(
self
.
user
.
id
)],
"anonymous"
:
[
"False"
],
"anonymous_to_peers"
:
[
"False"
],
"endorsed"
:
[
"False"
],
}
)
self
.
assertEqual
(
saved
[
"raw_body"
],
data
[
"raw_body"
])
def
test_update_empty_raw_body
(
self
):
serializer
=
CommentSerializer
(
self
.
existing_comment
,
data
=
{
"raw_body"
:
""
},
partial
=
True
,
context
=
get_context
(
self
.
course
,
self
.
request
)
)
self
.
assertEqual
(
serializer
.
errors
,
{
"raw_body"
:
[
"This field is required."
]}
)
@ddt.data
(
"thread_id"
,
"parent_id"
)
def
test_update_non_updatable
(
self
,
field
):
serializer
=
CommentSerializer
(
self
.
existing_comment
,
data
=
{
field
:
"different_value"
},
partial
=
True
,
context
=
get_context
(
self
.
course
,
self
.
request
)
)
self
.
assertEqual
(
serializer
.
errors
,
{
field
:
[
"This field is not allowed in an update."
]}
)
lms/djangoapps/discussion_api/tests/test_views.py
View file @
94c1bf49
...
...
@@ -13,7 +13,11 @@ from django.core.urlresolvers import reverse
from
rest_framework.test
import
APIClient
from
discussion_api.tests.utils
import
CommentsServiceMockMixin
,
make_minimal_cs_thread
from
discussion_api.tests.utils
import
(
CommentsServiceMockMixin
,
make_minimal_cs_comment
,
make_minimal_cs_thread
,
)
from
student.tests.factories
import
CourseEnrollmentFactory
,
UserFactory
from
util.testing
import
UrlResetMixin
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
...
...
@@ -633,3 +637,85 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
self
.
assertEqual
(
response
.
status_code
,
400
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
,
expected_response_data
)
class
CommentViewSetPartialUpdateTest
(
DiscussionAPIViewTestMixin
,
ModuleStoreTestCase
):
"""Tests for CommentViewSet partial_update"""
def
setUp
(
self
):
super
(
CommentViewSetPartialUpdateTest
,
self
)
.
setUp
()
httpretty
.
reset
()
httpretty
.
enable
()
self
.
addCleanup
(
httpretty
.
disable
)
self
.
register_get_user_response
(
self
.
user
)
self
.
url
=
reverse
(
"comment-detail"
,
kwargs
=
{
"comment_id"
:
"test_comment"
})
cs_thread
=
make_minimal_cs_thread
({
"id"
:
"test_thread"
,
"course_id"
:
unicode
(
self
.
course
.
id
),
})
self
.
register_get_thread_response
(
cs_thread
)
cs_comment
=
make_minimal_cs_comment
({
"id"
:
"test_comment"
,
"course_id"
:
cs_thread
[
"course_id"
],
"thread_id"
:
cs_thread
[
"id"
],
"username"
:
self
.
user
.
username
,
"user_id"
:
str
(
self
.
user
.
id
),
"created_at"
:
"2015-06-03T00:00:00Z"
,
"updated_at"
:
"2015-06-03T00:00:00Z"
,
"body"
:
"Original body"
,
})
self
.
register_get_comment_response
(
cs_comment
)
self
.
register_put_comment_response
(
cs_comment
)
def
test_basic
(
self
):
request_data
=
{
"raw_body"
:
"Edited body"
}
expected_response_data
=
{
"id"
:
"test_comment"
,
"thread_id"
:
"test_thread"
,
"parent_id"
:
None
,
"author"
:
self
.
user
.
username
,
"author_label"
:
None
,
"created_at"
:
"2015-06-03T00:00:00Z"
,
"updated_at"
:
"2015-06-03T00:00:00Z"
,
"raw_body"
:
"Edited body"
,
"endorsed"
:
False
,
"endorsed_by"
:
None
,
"endorsed_by_label"
:
None
,
"endorsed_at"
:
None
,
"abuse_flagged"
:
False
,
"voted"
:
False
,
"vote_count"
:
0
,
"children"
:
[],
}
response
=
self
.
client
.
patch
(
# pylint: disable=no-member
self
.
url
,
json
.
dumps
(
request_data
),
content_type
=
"application/json"
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
,
expected_response_data
)
self
.
assertEqual
(
httpretty
.
last_request
()
.
parsed_body
,
{
"body"
:
[
"Edited body"
],
"course_id"
:
[
unicode
(
self
.
course
.
id
)],
"user_id"
:
[
str
(
self
.
user
.
id
)],
"anonymous"
:
[
"False"
],
"anonymous_to_peers"
:
[
"False"
],
"endorsed"
:
[
"False"
],
}
)
def
test_error
(
self
):
request_data
=
{
"raw_body"
:
""
}
response
=
self
.
client
.
patch
(
# pylint: disable=no-member
self
.
url
,
json
.
dumps
(
request_data
),
content_type
=
"application/json"
)
expected_response_data
=
{
"field_errors"
:
{
"raw_body"
:
{
"developer_message"
:
"This field is required."
}}
}
self
.
assertEqual
(
response
.
status_code
,
400
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
,
expected_response_data
)
lms/djangoapps/discussion_api/tests/utils.py
View file @
94c1bf49
...
...
@@ -30,6 +30,32 @@ def _get_thread_callback(thread_data):
return
callback
def
_get_comment_callback
(
comment_data
,
thread_id
,
parent_id
):
"""
Get a callback function that will return a comment containing the given data
plus necessary dummy data, overridden by the content of the POST/PUT
request.
"""
def
callback
(
request
,
_uri
,
headers
):
"""
Simulate the comment creation or update endpoint as described above.
"""
response_data
=
make_minimal_cs_comment
(
comment_data
)
# thread_id and parent_id are not included in request payload but
# are returned by the comments service
response_data
[
"thread_id"
]
=
thread_id
response_data
[
"parent_id"
]
=
parent_id
for
key
,
val_list
in
request
.
parsed_body
.
items
():
val
=
val_list
[
0
]
if
key
in
[
"anonymous"
,
"anonymous_to_peers"
,
"endorsed"
]:
response_data
[
key
]
=
val
==
"True"
else
:
response_data
[
key
]
=
val
return
(
200
,
headers
,
json
.
dumps
(
response_data
))
return
callback
class
CommentsServiceMockMixin
(
object
):
"""Mixin with utility methods for mocking the comments service"""
def
register_get_threads_response
(
self
,
threads
,
page
,
num_pages
):
...
...
@@ -84,33 +110,35 @@ class CommentsServiceMockMixin(object):
status
=
200
)
def
register_post_comment_response
(
self
,
response_overrides
,
thread_id
,
parent_id
=
None
):
def
register_post_comment_response
(
self
,
comment_data
,
thread_id
,
parent_id
=
None
):
"""
Register a mock response for POST on the CS comments endpoint for the
given thread or parent; exactly one of thread_id and parent_id must be
specified.
"""
def
callback
(
request
,
_uri
,
headers
):
"""
Simulate the comment creation endpoint by returning the provided data
along with the data from response_overrides.
"""
response_data
=
make_minimal_cs_comment
(
{
key
:
val
[
0
]
for
key
,
val
in
request
.
parsed_body
.
items
()}
)
response_data
.
update
(
response_overrides
or
{})
# thread_id and parent_id are not included in request payload but
# are returned by the comments service
response_data
[
"thread_id"
]
=
thread_id
response_data
[
"parent_id"
]
=
parent_id
return
(
200
,
headers
,
json
.
dumps
(
response_data
))
if
parent_id
:
url
=
"http://localhost:4567/api/v1/comments/{}"
.
format
(
parent_id
)
else
:
url
=
"http://localhost:4567/api/v1/threads/{}/comments"
.
format
(
thread_id
)
httpretty
.
register_uri
(
httpretty
.
POST
,
url
,
body
=
callback
)
httpretty
.
register_uri
(
httpretty
.
POST
,
url
,
body
=
_get_comment_callback
(
comment_data
,
thread_id
,
parent_id
)
)
def
register_put_comment_response
(
self
,
comment_data
):
"""
Register a mock response for PUT on the CS endpoint for the given
comment data (which must include the key "id").
"""
thread_id
=
comment_data
[
"thread_id"
]
parent_id
=
comment_data
.
get
(
"parent_id"
)
httpretty
.
register_uri
(
httpretty
.
PUT
,
"http://localhost:4567/api/v1/comments/{}"
.
format
(
comment_data
[
"id"
]),
body
=
_get_comment_callback
(
comment_data
,
thread_id
,
parent_id
)
)
def
register_get_comment_error_response
(
self
,
comment_id
,
status_code
):
"""
...
...
lms/djangoapps/discussion_api/views.py
View file @
94c1bf49
...
...
@@ -18,6 +18,7 @@ from discussion_api.api import (
get_comment_list
,
get_course_topics
,
get_thread_list
,
update_comment
,
update_thread
,
)
from
discussion_api.forms
import
CommentListGetForm
,
ThreadListGetForm
...
...
@@ -212,7 +213,8 @@ class CommentViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
"""
**Use Cases**
Retrieve the list of comments in a thread.
Retrieve the list of comments in a thread, create a comment, or modify
an existing comment.
**Example Requests**:
...
...
@@ -224,6 +226,9 @@ class CommentViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
"raw_body": "Body text"
}
PATCH /api/discussion/v1/comments/comment_id
{"raw_body": "Edited text"}
**GET Parameters**:
* thread_id (required): The thread to retrieve comments for
...
...
@@ -245,6 +250,10 @@ class CommentViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
* raw_body: The comment's raw body text
**PATCH Parameters**:
raw_body is accepted with the same meaning as in a POST request
**GET Response Values**:
* results: The list of comments; each item in the list has the same
...
...
@@ -254,7 +263,7 @@ class CommentViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
* previous: The URL of the previous page (or null if last page)
**POST Response Values**:
**POST
/PATCH
Response Values**:
* id: The id of the comment
...
...
@@ -298,6 +307,8 @@ class CommentViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
* children: The list of child comments (with the same format)
"""
lookup_field
=
"comment_id"
def
list
(
self
,
request
):
"""
Implements the GET method for the list endpoint as described in the
...
...
@@ -322,3 +333,10 @@ class CommentViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
class docstring.
"""
return
Response
(
create_comment
(
request
,
request
.
DATA
))
def
partial_update
(
self
,
request
,
comment_id
):
"""
Implements the PATCH method for the instance endpoint as described in
the class docstring.
"""
return
Response
(
update_comment
(
request
,
comment_id
,
request
.
DATA
))
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