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
d6cd09be
Commit
d6cd09be
authored
Jun 01, 2015
by
Greg Price
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #8217 from edx/gprice/discussion-api-create-comment
Add comment creation to discussion API
parents
ef8f3918
8fbfa239
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
571 additions
and
50 deletions
+571
-50
lms/djangoapps/discussion_api/api.py
+47
-0
lms/djangoapps/discussion_api/serializers.py
+42
-4
lms/djangoapps/discussion_api/tests/test_api.py
+148
-1
lms/djangoapps/discussion_api/tests/test_serializers.py
+103
-0
lms/djangoapps/discussion_api/tests/test_views.py
+84
-0
lms/djangoapps/discussion_api/tests/utils.py
+50
-0
lms/djangoapps/discussion_api/views.py
+62
-31
lms/djangoapps/django_comment_client/base/views.py
+27
-10
lms/lib/comment_client/comment.py
+3
-3
openedx/core/lib/api/view_utils.py
+5
-1
No files found.
lms/djangoapps/discussion_api/api.py
View file @
d6cd09be
...
...
@@ -15,6 +15,8 @@ from discussion_api.pagination import get_paginated_data
from
discussion_api.serializers
import
CommentSerializer
,
ThreadSerializer
,
get_context
from
django_comment_client.base.views
import
(
THREAD_CREATED_EVENT_NAME
,
get_comment_created_event_data
,
get_comment_created_event_name
,
get_thread_created_event_data
,
track_forum_event
,
)
...
...
@@ -264,4 +266,49 @@ def create_thread(request, thread_data):
get_thread_created_event_data
(
thread
,
followed
=
following
)
)
return
ret
def
create_comment
(
request
,
comment_data
):
"""
Create a comment.
Parameters:
request: The django request object used for build_absolute_uri and
determining the requesting user.
comment_data: The data for the created comment.
Returns:
The created comment; see discussion_api.views.CommentViewSet for more
detail.
"""
thread_id
=
comment_data
.
get
(
"thread_id"
)
if
not
thread_id
:
raise
ValidationError
({
"thread_id"
:
[
"This field is required."
]})
try
:
thread
=
Thread
(
id
=
thread_id
)
.
retrieve
(
mark_as_read
=
False
)
course_key
=
CourseLocator
.
from_string
(
thread
[
"course_id"
])
course
=
_get_course_or_404
(
course_key
,
request
.
user
)
except
(
Http404
,
CommentClientRequestError
):
raise
ValidationError
({
"thread_id"
:
[
"Invalid value."
]})
parent_id
=
comment_data
.
get
(
"parent_id"
)
context
=
get_context
(
course
,
request
,
thread
,
parent_id
)
serializer
=
CommentSerializer
(
data
=
comment_data
,
context
=
context
)
if
not
serializer
.
is_valid
():
raise
ValidationError
(
serializer
.
errors
)
serializer
.
save
()
comment
=
serializer
.
object
track_forum_event
(
request
,
get_comment_created_event_name
(
comment
),
course
,
comment
,
get_comment_created_event_data
(
comment
,
thread
[
"commentable_id"
],
followed
=
False
)
)
return
serializer
.
data
lms/djangoapps/discussion_api/serializers.py
View file @
d6cd09be
...
...
@@ -5,6 +5,7 @@ from urllib import urlencode
from
urlparse
import
urlunparse
from
django.contrib.auth.models
import
User
as
DjangoUser
from
django.core.exceptions
import
ValidationError
from
django.core.urlresolvers
import
reverse
from
rest_framework
import
serializers
...
...
@@ -15,12 +16,14 @@ from django_comment_common.models import (
FORUM_ROLE_MODERATOR
,
Role
,
)
from
lms.lib.comment_client.comment
import
Comment
from
lms.lib.comment_client.thread
import
Thread
from
lms.lib.comment_client.user
import
User
as
CommentClientUser
from
lms.lib.comment_client.utils
import
CommentClientRequestError
from
openedx.core.djangoapps.course_groups.cohorts
import
get_cohort_names
def
get_context
(
course
,
request
,
thread
=
None
):
def
get_context
(
course
,
request
,
thread
=
None
,
parent_id
=
None
):
"""
Returns a context appropriate for use with ThreadSerializer or
(if thread is provided) CommentSerializer.
...
...
@@ -49,6 +52,7 @@ def get_context(course, request, thread=None):
"ta_user_ids"
:
ta_user_ids
,
"cc_requester"
:
CommentClientUser
.
from_django_user
(
requester
)
.
retrieve
(),
"thread"
:
thread
,
"parent_id"
:
parent_id
,
}
...
...
@@ -204,13 +208,16 @@ class CommentSerializer(_ContentSerializer):
"""
A serializer for comment data.
Because it is not a field in the underlying data, parent_id must be provided
in the context for both serialization and deserialization.
N.B. This should not be used with a comment_client Comment object that has
not had retrieve() called, because of the interaction between DRF's attempts
at introspection and Comment's __getattr__.
"""
thread_id
=
serializers
.
CharField
()
parent_id
=
serializers
.
SerializerMethodField
(
"get_parent_id"
)
endorsed
=
serializers
.
BooleanField
()
endorsed
=
serializers
.
BooleanField
(
read_only
=
True
)
endorsed_by
=
serializers
.
SerializerMethodField
(
"get_endorsed_by"
)
endorsed_by_label
=
serializers
.
SerializerMethodField
(
"get_endorsed_by_label"
)
endorsed_at
=
serializers
.
SerializerMethodField
(
"get_endorsed_at"
)
...
...
@@ -218,7 +225,7 @@ class CommentSerializer(_ContentSerializer):
def
get_parent_id
(
self
,
_obj
):
"""Returns the comment's parent's id (taken from the context)."""
return
self
.
context
.
get
(
"parent_id"
)
return
self
.
context
[
"parent_id"
]
def
get_endorsed_by
(
self
,
obj
):
"""
...
...
@@ -257,4 +264,35 @@ class CommentSerializer(_ContentSerializer):
"""Returns the list of the comment's children, serialized."""
child_context
=
dict
(
self
.
context
)
child_context
[
"parent_id"
]
=
obj
[
"id"
]
return
[
CommentSerializer
(
child
,
context
=
child_context
)
.
data
for
child
in
obj
[
"children"
]]
return
[
CommentSerializer
(
child
,
context
=
child_context
)
.
data
for
child
in
obj
.
get
(
"children"
,
[])
]
def
validate
(
self
,
attrs
):
"""
Ensure that parent_id identifies a comment that is actually in the
thread identified by thread_id.
"""
parent_id
=
self
.
context
[
"parent_id"
]
if
parent_id
:
parent
=
None
try
:
parent
=
Comment
(
id
=
parent_id
)
.
retrieve
()
except
CommentClientRequestError
:
pass
if
not
(
parent
and
parent
[
"thread_id"
]
==
attrs
[
"thread_id"
]):
raise
ValidationError
(
"parent_id does not identify a comment in the thread identified by thread_id."
)
return
attrs
def
restore_object
(
self
,
attrs
,
instance
=
None
):
if
instance
:
# pragma: no cover
raise
ValueError
(
"CommentSerializer cannot be used for updates."
)
return
Comment
(
course_id
=
self
.
context
[
"thread"
][
"course_id"
],
parent_id
=
self
.
context
[
"parent_id"
],
user_id
=
self
.
context
[
"cc_requester"
][
"id"
],
**
attrs
)
lms/djangoapps/discussion_api/tests/test_api.py
View file @
d6cd09be
...
...
@@ -17,7 +17,13 @@ from django.test.client import RequestFactory
from
opaque_keys.edx.locator
import
CourseLocator
from
courseware.tests.factories
import
BetaTesterFactory
,
StaffFactory
from
discussion_api.api
import
create_thread
,
get_comment_list
,
get_course_topics
,
get_thread_list
from
discussion_api.api
import
(
create_comment
,
create_thread
,
get_comment_list
,
get_course_topics
,
get_thread_list
,
)
from
discussion_api.tests.utils
import
(
CommentsServiceMockMixin
,
make_minimal_cs_comment
,
...
...
@@ -1116,3 +1122,144 @@ class CreateThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestC
data
[
"type"
]
=
"invalid_type"
with
self
.
assertRaises
(
ValidationError
):
create_thread
(
self
.
request
,
data
)
@ddt.ddt
class
CreateCommentTest
(
CommentsServiceMockMixin
,
UrlResetMixin
,
ModuleStoreTestCase
):
"""Tests for create_comment"""
@mock.patch.dict
(
"django.conf.settings.FEATURES"
,
{
"ENABLE_DISCUSSION_SERVICE"
:
True
})
def
setUp
(
self
):
super
(
CreateCommentTest
,
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
)
self
.
register_get_thread_response
(
make_minimal_cs_thread
({
"id"
:
"test_thread"
,
"course_id"
:
unicode
(
self
.
course
.
id
),
"commentable_id"
:
"test_topic"
,
})
)
self
.
minimal_data
=
{
"thread_id"
:
"test_thread"
,
"raw_body"
:
"Test body"
,
}
@ddt.data
(
None
,
"test_parent"
)
@mock.patch
(
"eventtracking.tracker.emit"
)
def
test_success
(
self
,
parent_id
,
mock_emit
):
if
parent_id
:
self
.
register_get_comment_response
({
"id"
:
parent_id
,
"thread_id"
:
"test_thread"
})
self
.
register_post_comment_response
(
{
"id"
:
"test_comment"
,
"thread_id"
:
"test_thread"
,
"username"
:
self
.
user
.
username
,
"created_at"
:
"2015-05-27T00:00:00Z"
,
"updated_at"
:
"2015-05-27T00:00:00Z"
,
},
thread_id
=
(
None
if
parent_id
else
"test_thread"
),
parent_id
=
parent_id
)
data
=
self
.
minimal_data
.
copy
()
if
parent_id
:
data
[
"parent_id"
]
=
parent_id
actual
=
create_comment
(
self
.
request
,
data
)
expected
=
{
"id"
:
"test_comment"
,
"thread_id"
:
"test_thread"
,
"parent_id"
:
parent_id
,
"author"
:
self
.
user
.
username
,
"author_label"
:
None
,
"created_at"
:
"2015-05-27T00:00:00Z"
,
"updated_at"
:
"2015-05-27T00:00:00Z"
,
"raw_body"
:
"Test 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
)
expected_url
=
(
"/api/v1/comments/{}"
.
format
(
parent_id
)
if
parent_id
else
"/api/v1/threads/test_thread/comments"
)
self
.
assertEqual
(
urlparse
(
httpretty
.
last_request
()
.
path
)
.
path
,
expected_url
)
self
.
assertEqual
(
httpretty
.
last_request
()
.
parsed_body
,
{
"course_id"
:
[
unicode
(
self
.
course
.
id
)],
"body"
:
[
"Test body"
],
"user_id"
:
[
str
(
self
.
user
.
id
)]
}
)
expected_event_name
=
(
"edx.forum.comment.created"
if
parent_id
else
"edx.forum.response.created"
)
expected_event_data
=
{
"discussion"
:
{
"id"
:
"test_thread"
},
"commentable_id"
:
"test_topic"
,
"options"
:
{
"followed"
:
False
},
"id"
:
"test_comment"
,
"truncated"
:
False
,
"body"
:
"Test body"
,
"url"
:
""
,
"user_forums_roles"
:
[
FORUM_ROLE_STUDENT
],
"user_course_roles"
:
[],
}
if
parent_id
:
expected_event_data
[
"response"
]
=
{
"id"
:
parent_id
}
actual_event_name
,
actual_event_data
=
mock_emit
.
call_args
[
0
]
self
.
assertEqual
(
actual_event_name
,
expected_event_name
)
self
.
assertEqual
(
actual_event_data
,
expected_event_data
)
def
test_thread_id_missing
(
self
):
with
self
.
assertRaises
(
ValidationError
)
as
assertion
:
create_comment
(
self
.
request
,
{})
self
.
assertEqual
(
assertion
.
exception
.
message_dict
,
{
"thread_id"
:
[
"This field is required."
]})
def
test_thread_id_not_found
(
self
):
self
.
register_get_thread_error_response
(
"test_thread"
,
404
)
with
self
.
assertRaises
(
ValidationError
)
as
assertion
:
create_comment
(
self
.
request
,
self
.
minimal_data
)
self
.
assertEqual
(
assertion
.
exception
.
message_dict
,
{
"thread_id"
:
[
"Invalid value."
]})
def
test_nonexistent_course
(
self
):
self
.
register_get_thread_response
(
make_minimal_cs_thread
({
"id"
:
"test_thread"
,
"course_id"
:
"non/existent/course"
})
)
with
self
.
assertRaises
(
ValidationError
)
as
assertion
:
create_comment
(
self
.
request
,
self
.
minimal_data
)
self
.
assertEqual
(
assertion
.
exception
.
message_dict
,
{
"thread_id"
:
[
"Invalid value."
]})
def
test_not_enrolled
(
self
):
self
.
request
.
user
=
UserFactory
.
create
()
with
self
.
assertRaises
(
ValidationError
)
as
assertion
:
create_comment
(
self
.
request
,
self
.
minimal_data
)
self
.
assertEqual
(
assertion
.
exception
.
message_dict
,
{
"thread_id"
:
[
"Invalid value."
]})
def
test_discussions_disabled
(
self
):
_remove_discussion_tab
(
self
.
course
,
self
.
user
.
id
)
with
self
.
assertRaises
(
ValidationError
)
as
assertion
:
create_comment
(
self
.
request
,
self
.
minimal_data
)
self
.
assertEqual
(
assertion
.
exception
.
message_dict
,
{
"thread_id"
:
[
"Invalid value."
]})
def
test_invalid_field
(
self
):
data
=
self
.
minimal_data
.
copy
()
del
data
[
"raw_body"
]
with
self
.
assertRaises
(
ValidationError
):
create_comment
(
self
.
request
,
data
)
lms/djangoapps/discussion_api/tests/test_serializers.py
View file @
d6cd09be
...
...
@@ -440,3 +440,106 @@ class ThreadSerializerDeserializationTest(CommentsServiceMockMixin, UrlResetMixi
data
[
"type"
]
=
"invalid_type"
serializer
=
ThreadSerializer
(
data
=
data
)
self
.
assertFalse
(
serializer
.
is_valid
())
@ddt.ddt
class
CommentSerializerDeserializationTest
(
CommentsServiceMockMixin
,
ModuleStoreTestCase
):
"""Tests for ThreadSerializer deserialization."""
def
setUp
(
self
):
super
(
CommentSerializerDeserializationTest
,
self
)
.
setUp
()
httpretty
.
reset
()
httpretty
.
enable
()
self
.
addCleanup
(
httpretty
.
disable
)
self
.
course
=
CourseFactory
.
create
()
self
.
user
=
UserFactory
.
create
()
self
.
register_get_user_response
(
self
.
user
)
self
.
request
=
RequestFactory
()
.
get
(
"/dummy"
)
self
.
request
.
user
=
self
.
user
self
.
minimal_data
=
{
"thread_id"
:
"test_thread"
,
"raw_body"
:
"Test body"
,
}
def
save_and_reserialize
(
self
,
data
,
parent_id
=
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.
"""
context
=
get_context
(
self
.
course
,
self
.
request
,
make_minimal_cs_thread
({
"course_id"
:
unicode
(
self
.
course
.
id
)}),
parent_id
)
serializer
=
CommentSerializer
(
data
=
data
,
context
=
context
)
self
.
assertTrue
(
serializer
.
is_valid
())
serializer
.
save
()
return
serializer
.
data
@ddt.data
(
None
,
"test_parent"
)
def
test_success
(
self
,
parent_id
):
if
parent_id
:
self
.
register_get_comment_response
({
"thread_id"
:
"test_thread"
,
"id"
:
parent_id
})
self
.
register_post_comment_response
(
{
"id"
:
"test_comment"
},
thread_id
=
(
None
if
parent_id
else
"test_thread"
),
parent_id
=
parent_id
)
saved
=
self
.
save_and_reserialize
(
self
.
minimal_data
,
parent_id
=
parent_id
)
expected_url
=
(
"/api/v1/comments/{}"
.
format
(
parent_id
)
if
parent_id
else
"/api/v1/threads/test_thread/comments"
)
self
.
assertEqual
(
urlparse
(
httpretty
.
last_request
()
.
path
)
.
path
,
expected_url
)
self
.
assertEqual
(
httpretty
.
last_request
()
.
parsed_body
,
{
"course_id"
:
[
unicode
(
self
.
course
.
id
)],
"body"
:
[
"Test body"
],
"user_id"
:
[
str
(
self
.
user
.
id
)],
}
)
self
.
assertEqual
(
saved
[
"id"
],
"test_comment"
)
self
.
assertEqual
(
saved
[
"parent_id"
],
parent_id
)
def
test_parent_id_nonexistent
(
self
):
self
.
register_get_comment_error_response
(
"bad_parent"
,
404
)
context
=
get_context
(
self
.
course
,
self
.
request
,
make_minimal_cs_thread
(),
"bad_parent"
)
serializer
=
CommentSerializer
(
data
=
self
.
minimal_data
,
context
=
context
)
self
.
assertFalse
(
serializer
.
is_valid
())
self
.
assertEqual
(
serializer
.
errors
,
{
"non_field_errors"
:
[
"parent_id does not identify a comment in the thread identified by thread_id."
]
}
)
def
test_parent_id_wrong_thread
(
self
):
self
.
register_get_comment_response
({
"thread_id"
:
"different_thread"
,
"id"
:
"test_parent"
})
context
=
get_context
(
self
.
course
,
self
.
request
,
make_minimal_cs_thread
(),
"test_parent"
)
serializer
=
CommentSerializer
(
data
=
self
.
minimal_data
,
context
=
context
)
self
.
assertFalse
(
serializer
.
is_valid
())
self
.
assertEqual
(
serializer
.
errors
,
{
"non_field_errors"
:
[
"parent_id does not identify a comment in the thread identified by thread_id."
]
}
)
def
test_missing_field
(
self
):
for
field
in
self
.
minimal_data
:
data
=
self
.
minimal_data
.
copy
()
data
.
pop
(
field
)
serializer
=
CommentSerializer
(
data
=
data
,
context
=
get_context
(
self
.
course
,
self
.
request
,
make_minimal_cs_thread
())
)
self
.
assertFalse
(
serializer
.
is_valid
())
self
.
assertEqual
(
serializer
.
errors
,
{
field
:
[
"This field is required."
]}
)
lms/djangoapps/discussion_api/tests/test_views.py
View file @
d6cd09be
...
...
@@ -3,6 +3,7 @@ Tests for Discussion API views
"""
from
datetime
import
datetime
import
json
from
urlparse
import
urlparse
import
httpretty
import
mock
...
...
@@ -416,3 +417,86 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"mark_as_read"
:
[
"True"
],
}
)
@httpretty.activate
class
CommentViewSetCreateTest
(
DiscussionAPIViewTestMixin
,
ModuleStoreTestCase
):
"""Tests for CommentViewSet create"""
def
setUp
(
self
):
super
(
CommentViewSetCreateTest
,
self
)
.
setUp
()
self
.
url
=
reverse
(
"comment-list"
)
def
test_basic
(
self
):
self
.
register_get_user_response
(
self
.
user
)
self
.
register_get_thread_response
(
make_minimal_cs_thread
({
"id"
:
"test_thread"
,
"course_id"
:
unicode
(
self
.
course
.
id
),
"commentable_id"
:
"test_topic"
,
})
)
self
.
register_post_comment_response
(
{
"id"
:
"test_comment"
,
"thread_id"
:
"test_thread"
,
"username"
:
self
.
user
.
username
,
"created_at"
:
"2015-05-27T00:00:00Z"
,
"updated_at"
:
"2015-05-27T00:00:00Z"
,
},
thread_id
=
"test_thread"
)
request_data
=
{
"thread_id"
:
"test_thread"
,
"raw_body"
:
"Test body"
,
}
expected_response_data
=
{
"id"
:
"test_comment"
,
"thread_id"
:
"test_thread"
,
"parent_id"
:
None
,
"author"
:
self
.
user
.
username
,
"author_label"
:
None
,
"created_at"
:
"2015-05-27T00:00:00Z"
,
"updated_at"
:
"2015-05-27T00:00:00Z"
,
"raw_body"
:
"Test 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
.
post
(
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
(
urlparse
(
httpretty
.
last_request
()
.
path
)
.
path
,
"/api/v1/threads/test_thread/comments"
)
self
.
assertEqual
(
httpretty
.
last_request
()
.
parsed_body
,
{
"course_id"
:
[
unicode
(
self
.
course
.
id
)],
"body"
:
[
"Test body"
],
"user_id"
:
[
str
(
self
.
user
.
id
)],
}
)
def
test_error
(
self
):
response
=
self
.
client
.
post
(
self
.
url
,
json
.
dumps
({}),
content_type
=
"application/json"
)
expected_response_data
=
{
"field_errors"
:
{
"thread_id"
:
{
"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 @
d6cd09be
...
...
@@ -61,6 +61,56 @@ class CommentsServiceMockMixin(object):
status
=
200
)
def
register_post_comment_response
(
self
,
response_overrides
,
thread_id
=
None
,
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
{})
return
(
200
,
headers
,
json
.
dumps
(
response_data
))
if
thread_id
and
not
parent_id
:
url
=
"http://localhost:4567/api/v1/threads/{}/comments"
.
format
(
thread_id
)
elif
parent_id
and
not
thread_id
:
url
=
"http://localhost:4567/api/v1/comments/{}"
.
format
(
parent_id
)
else
:
# pragma: no cover
raise
ValueError
(
"Exactly one of thread_id and parent_id must be provided."
)
httpretty
.
register_uri
(
httpretty
.
POST
,
url
,
body
=
callback
)
def
register_get_comment_error_response
(
self
,
comment_id
,
status_code
):
"""
Register a mock error response for GET on the CS comment instance
endpoint.
"""
httpretty
.
register_uri
(
httpretty
.
GET
,
"http://localhost:4567/api/v1/comments/{id}"
.
format
(
id
=
comment_id
),
body
=
""
,
status
=
status_code
)
def
register_get_comment_response
(
self
,
response_overrides
):
"""
Register a mock response for GET on the CS comment instance endpoint.
"""
comment
=
make_minimal_cs_comment
(
response_overrides
)
httpretty
.
register_uri
(
httpretty
.
GET
,
"http://localhost:4567/api/v1/comments/{id}"
.
format
(
id
=
comment
[
"id"
]),
body
=
json
.
dumps
(
comment
),
status
=
200
)
def
register_get_user_response
(
self
,
user
,
subscribed_thread_ids
=
None
,
upvoted_ids
=
None
):
"""Register a mock response for GET on the CS user instance endpoint"""
httpretty
.
register_uri
(
...
...
lms/djangoapps/discussion_api/views.py
View file @
d6cd09be
...
...
@@ -11,7 +11,13 @@ from rest_framework.viewsets import ViewSet
from
opaque_keys.edx.locator
import
CourseLocator
from
discussion_api.api
import
create_thread
,
get_comment_list
,
get_course_topics
,
get_thread_list
from
discussion_api.api
import
(
create_comment
,
create_thread
,
get_comment_list
,
get_course_topics
,
get_thread_list
,
)
from
discussion_api.forms
import
CommentListGetForm
,
ThreadListGetForm
from
openedx.core.lib.api.view_utils
import
DeveloperErrorViewMixin
...
...
@@ -173,6 +179,12 @@ class CommentViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
GET /api/discussion/v1/comments/?thread_id=0123456789abcdef01234567
POST /api/discussion/v1/comments/
{
"thread_id": "0123456789abcdef01234567",
"raw_body": "Body text"
}
**GET Parameters**:
* thread_id (required): The thread to retrieve comments for
...
...
@@ -185,55 +197,67 @@ class CommentViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
* page_size: The number of items per page (default is 10, max is 100)
**
Response Value
s**:
**
POST Parameter
s**:
*
results: The list of comments. Each item in the list includes:
*
thread_id (required): The thread to post the comment in
* id: The id of the comment
* parent_id: The parent comment of the new comment. Can be null or
omitted for a comment that should be directly under the thread
* thread_id: The id of the comment's thread
* raw_body: The comment's raw body text
* parent_id: The id of the comment's parent
**GET Response Values**:
* author: The username of the comment's author, or None if th
e
comment is anonymous
* results: The list of comments; each item in the list has the sam
e
fields as the POST response below
* author_label: A label indicating whether the author has a special
role in the course, either "staff" for moderators and
administrators or "community_ta" for community TAs
* next: The URL of the next page (or null if first page)
* created_at: The ISO 8601 timestamp for the creation of the comment
* previous: The URL of the previous page (or null if last page)
* updated_at: The ISO 8601 timestamp for the last modification of
the comment, which may not have been an update of the body
**POST Response Values**:
* raw_body: The comment's raw body text without any rendering applied
* id: The id of the comment
* endorsed: Boolean indicating whether the comment has been endorsed
(by a privileged user or, for a question thread, the thread
author)
* thread_id: The id of the comment's thread
* endorsed_by: The username of the endorsing user, if available
* parent_id: The id of the comment's parent
* endorsed_by_label: A label indicating whether the endorsing user
has a special role in the course (see author_label)
* author: The username of the comment's author, or None if the
comment is anonymous
* endorsed_at: The ISO 8601 timestamp for the endorsement, if
available
* author_label: A label indicating whether the author has a special
role in the course, either "staff" for moderators and
administrators or "community_ta" for community TAs
* abuse_flagged: Boolean indicating whether the requesting user has
flagged the comment for abuse
* created_at: The ISO 8601 timestamp for the creation of the comment
* voted: Boolean indicating whether the requesting user has voted
for the comment
* updated_at: The ISO 8601 timestamp for the last modification of
the comment, which may not have been an update of the body
* vote_count: The number of votes for the comment
* raw_body: The comment's raw body text without any rendering applied
* children: The list of child comments (with the same format)
* endorsed: Boolean indicating whether the comment has been endorsed
(by a privileged user or, for a question thread, the thread
author)
*
next: The URL of the next page (or null if first page)
*
endorsed_by: The username of the endorsing user, if available
* previous: The URL of the previous page (or null if last page)
* endorsed_by_label: A label indicating whether the endorsing user
has a special role in the course (see author_label)
* endorsed_at: The ISO 8601 timestamp for the endorsement, if
available
* abuse_flagged: Boolean indicating whether the requesting user has
flagged the comment for abuse
* voted: Boolean indicating whether the requesting user has voted
for the comment
* vote_count: The number of votes for the comment
* children: The list of child comments (with the same format)
"""
def
list
(
self
,
request
):
"""
...
...
@@ -252,3 +276,10 @@ class CommentViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
form
.
cleaned_data
[
"page_size"
]
)
)
def
create
(
self
,
request
):
"""
Implements the POST method for the list endpoint as described in the
class docstring.
"""
return
Response
(
create_comment
(
request
,
request
.
DATA
))
lms/djangoapps/django_comment_client/base/views.py
View file @
d6cd09be
...
...
@@ -38,6 +38,8 @@ log = logging.getLogger(__name__)
TRACKING_MAX_FORUM_BODY
=
2000
THREAD_CREATED_EVENT_NAME
=
"edx.forum.thread.created"
RESPONSE_CREATED_EVENT_NAME
=
'edx.forum.response.created'
COMMENT_CREATED_EVENT_NAME
=
'edx.forum.comment.created'
def
permitted
(
fn
):
...
...
@@ -119,6 +121,29 @@ def get_thread_created_event_data(thread, followed):
}
def
get_comment_created_event_name
(
comment
):
"""Get the appropriate event name for creating a response/comment"""
return
COMMENT_CREATED_EVENT_NAME
if
comment
.
get
(
"parent_id"
)
else
RESPONSE_CREATED_EVENT_NAME
def
get_comment_created_event_data
(
comment
,
commentable_id
,
followed
):
"""
Get the event data payload for comment creation (excluding fields populated
by track_forum_event)
"""
event_data
=
{
'discussion'
:
{
'id'
:
comment
.
thread_id
},
'commentable_id'
:
commentable_id
,
'options'
:
{
'followed'
:
followed
},
}
parent_id
=
comment
.
get
(
"parent_id"
)
if
parent_id
:
event_data
[
'response'
]
=
{
'id'
:
parent_id
}
return
event_data
@require_POST
@login_required
@permitted
...
...
@@ -270,16 +295,8 @@ def _create_comment(request, course_key, thread_id=None, parent_id=None):
user
=
cc
.
User
.
from_django_user
(
request
.
user
)
user
.
follow
(
comment
.
thread
)
event_data
=
{
'discussion'
:
{
'id'
:
comment
.
thread_id
},
'options'
:
{
'followed'
:
followed
}}
if
parent_id
:
event_data
[
'response'
]
=
{
'id'
:
comment
.
parent_id
}
event_name
=
'edx.forum.comment.created'
else
:
event_name
=
'edx.forum.response.created'
event_data
[
'commentable_id'
]
=
comment
.
thread
.
commentable_id
event_name
=
get_comment_created_event_name
(
comment
)
event_data
=
get_comment_created_event_data
(
comment
,
comment
.
thread
.
commentable_id
,
followed
)
track_forum_event
(
request
,
event_name
,
course
,
comment
,
event_data
)
if
request
.
is_ajax
():
...
...
lms/lib/comment_client/comment.py
View file @
d6cd09be
...
...
@@ -32,10 +32,10 @@ class Comment(models.Model):
@classmethod
def
url_for_comments
(
cls
,
params
=
{}):
if
params
.
get
(
'thread_id'
):
return
_url_for_thread_comments
(
params
[
'thread_id'
])
else
:
if
params
.
get
(
'parent_id'
):
return
_url_for_comment
(
params
[
'parent_id'
])
else
:
return
_url_for_thread_comments
(
params
[
'thread_id'
])
@classmethod
def
url
(
cls
,
action
,
params
=
{}):
...
...
openedx/core/lib/api/view_utils.py
View file @
d6cd09be
...
...
@@ -40,7 +40,11 @@ class DeveloperErrorViewMixin(object):
if
hasattr
(
validation_error
,
"message_dict"
):
response_obj
=
{}
message_dict
=
dict
(
validation_error
.
message_dict
)
non_field_error_list
=
message_dict
.
pop
(
NON_FIELD_ERRORS
,
None
)
# Extract both Django form and DRF serializer non-field errors
non_field_error_list
=
(
message_dict
.
pop
(
NON_FIELD_ERRORS
,
[])
+
message_dict
.
pop
(
"non_field_errors"
,
[])
)
if
non_field_error_list
:
response_obj
[
"developer_message"
]
=
non_field_error_list
[
0
]
if
message_dict
:
...
...
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