Commit 16d7a81b by Greg Price

Add editable_fields to discussion API responses

This will allow clients to determine what actions (e.g. following,
endorsing, and editing content) a user can take on a piece of content.
parent 7c534001
...@@ -17,6 +17,7 @@ from opaque_keys.edx.locator import CourseKey ...@@ -17,6 +17,7 @@ from opaque_keys.edx.locator import CourseKey
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
from discussion_api.forms import CommentActionsForm, ThreadActionsForm from discussion_api.forms import CommentActionsForm, ThreadActionsForm
from discussion_api.pagination import get_paginated_data from discussion_api.pagination import get_paginated_data
from discussion_api.permissions import can_delete, get_editable_fields
from discussion_api.serializers import CommentSerializer, ThreadSerializer, get_context from discussion_api.serializers import CommentSerializer, ThreadSerializer, get_context
from django_comment_client.base.views import ( from django_comment_client.base.views import (
THREAD_CREATED_EVENT_NAME, THREAD_CREATED_EVENT_NAME,
...@@ -91,19 +92,6 @@ def _get_comment_and_context(request, comment_id): ...@@ -91,19 +92,6 @@ def _get_comment_and_context(request, comment_id):
raise Http404 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_content["user_id"]
)
def get_thread_list_url(request, course_key, topic_id_list=None): def get_thread_list_url(request, course_key, topic_id_list=None):
""" """
Returns the URL for the thread_list_url field, given a list of topic_ids Returns the URL for the thread_list_url field, given a list of topic_ids
...@@ -352,6 +340,21 @@ def get_comment_list(request, thread_id, endorsed, page, page_size): ...@@ -352,6 +340,21 @@ def get_comment_list(request, thread_id, endorsed, page, page_size):
return get_paginated_data(request, results, page, num_pages) return get_paginated_data(request, results, page, num_pages)
def _check_editable_fields(cc_content, data, context):
"""
Raise ValidationError if the given update data contains a field that is not
in editable_fields.
"""
editable_fields = get_editable_fields(cc_content, context)
non_editable_errors = {
field: ["This field is not editable."]
for field in data.keys()
if field not in editable_fields
}
if non_editable_errors:
raise ValidationError(non_editable_errors)
def _do_extra_actions(api_content, cc_content, request_fields, actions_form, context): def _do_extra_actions(api_content, cc_content, request_fields, actions_form, context):
""" """
Perform any necessary additional actions related to content creation or Perform any necessary additional actions related to content creation or
...@@ -462,35 +465,7 @@ def create_comment(request, comment_data): ...@@ -462,35 +465,7 @@ def create_comment(request, comment_data):
get_comment_created_event_data(cc_comment, cc_thread["commentable_id"], followed=False) get_comment_created_event_data(cc_comment, cc_thread["commentable_id"], followed=False)
) )
return serializer.data return api_comment
_THREAD_EDITABLE_BY_ANY = {"following", "voted"}
_THREAD_EDITABLE_BY_AUTHOR = {"topic_id", "type", "title", "raw_body"} | _THREAD_EDITABLE_BY_ANY
def _get_thread_editable_fields(cc_thread, context):
"""
Get the list of editable fields for the given thread in the given context
"""
if _is_user_author_or_privileged(cc_thread, context):
return _THREAD_EDITABLE_BY_AUTHOR
else:
return _THREAD_EDITABLE_BY_ANY
def _check_editable_fields(editable_fields, update_data):
"""
Raise ValidationError if the given update data contains a field that is not
in editable_fields.
"""
non_editable_errors = {
field: ["This field is not editable."]
for field in update_data.keys()
if field not in editable_fields
}
if non_editable_errors:
raise ValidationError(non_editable_errors)
def update_thread(request, thread_id, update_data): def update_thread(request, thread_id, update_data):
...@@ -512,8 +487,7 @@ def update_thread(request, thread_id, update_data): ...@@ -512,8 +487,7 @@ def update_thread(request, thread_id, update_data):
detail. detail.
""" """
cc_thread, context = _get_thread_and_context(request, thread_id) cc_thread, context = _get_thread_and_context(request, thread_id)
editable_fields = _get_thread_editable_fields(cc_thread, context) _check_editable_fields(cc_thread, update_data, context)
_check_editable_fields(editable_fields, update_data)
serializer = ThreadSerializer(cc_thread, data=update_data, partial=True, context=context) serializer = ThreadSerializer(cc_thread, data=update_data, partial=True, context=context)
actions_form = ThreadActionsForm(update_data) actions_form = ThreadActionsForm(update_data)
if not (serializer.is_valid() and actions_form.is_valid()): if not (serializer.is_valid() and actions_form.is_valid()):
...@@ -526,22 +500,6 @@ def update_thread(request, thread_id, update_data): ...@@ -526,22 +500,6 @@ def update_thread(request, thread_id, update_data):
return api_thread return api_thread
_COMMENT_EDITABLE_BY_AUTHOR = {"raw_body"}
_COMMENT_EDITABLE_BY_THREAD_AUTHOR = {"endorsed"}
def _get_comment_editable_fields(cc_comment, context):
"""
Get the list of editable fields for the given comment in the given context
"""
ret = {"voted"}
if _is_user_author_or_privileged(cc_comment, context):
ret |= _COMMENT_EDITABLE_BY_AUTHOR
if _is_user_author_or_privileged(context["thread"], context):
ret |= _COMMENT_EDITABLE_BY_THREAD_AUTHOR
return ret
def update_comment(request, comment_id, update_data): def update_comment(request, comment_id, update_data):
""" """
Update a comment. Update a comment.
...@@ -572,13 +530,12 @@ def update_comment(request, comment_id, update_data): ...@@ -572,13 +530,12 @@ def update_comment(request, comment_id, update_data):
is empty or thread_id is included) is empty or thread_id is included)
""" """
cc_comment, context = _get_comment_and_context(request, comment_id) cc_comment, context = _get_comment_and_context(request, comment_id)
editable_fields = _get_comment_editable_fields(cc_comment, context) _check_editable_fields(cc_comment, update_data, context)
_check_editable_fields(editable_fields, update_data)
serializer = CommentSerializer(cc_comment, data=update_data, partial=True, context=context) serializer = CommentSerializer(cc_comment, data=update_data, partial=True, context=context)
actions_form = CommentActionsForm(update_data) actions_form = CommentActionsForm(update_data)
if not (serializer.is_valid() and actions_form.is_valid()): if not (serializer.is_valid() and actions_form.is_valid()):
raise ValidationError(dict(serializer.errors.items() + actions_form.errors.items())) raise ValidationError(dict(serializer.errors.items() + actions_form.errors.items()))
# Only save thread object if some of the edited fields are in the thread data, not extra actions # Only save comment object if some of the edited fields are in the comment data, not extra actions
if set(update_data) - set(actions_form.fields): if set(update_data) - set(actions_form.fields):
serializer.save() serializer.save()
api_comment = serializer.data api_comment = serializer.data
...@@ -603,7 +560,7 @@ def delete_thread(request, thread_id): ...@@ -603,7 +560,7 @@ def delete_thread(request, thread_id):
""" """
cc_thread, context = _get_thread_and_context(request, thread_id) cc_thread, context = _get_thread_and_context(request, thread_id)
if _is_user_author_or_privileged(cc_thread, context): if can_delete(cc_thread, context):
cc_thread.delete() cc_thread.delete()
else: else:
raise PermissionDenied raise PermissionDenied
...@@ -626,7 +583,7 @@ def delete_comment(request, comment_id): ...@@ -626,7 +583,7 @@ def delete_comment(request, comment_id):
""" """
cc_comment, context = _get_comment_and_context(request, comment_id) cc_comment, context = _get_comment_and_context(request, comment_id)
if _is_user_author_or_privileged(cc_comment, context): if can_delete(cc_comment, context):
cc_comment.delete() cc_comment.delete()
else: else:
raise PermissionDenied raise PermissionDenied
"""
Discussion API permission logic
"""
def _is_author(cc_content, context):
"""
Return True if the requester authored the given content, False otherwise
"""
return context["cc_requester"]["id"] == cc_content["user_id"]
def _is_author_or_privileged(cc_content, context):
"""
Return True if the requester authored the given content or is a privileged
user, False otherwise
"""
return context["is_requester_privileged"] or _is_author(cc_content, context)
def get_editable_fields(cc_content, context):
"""
Return the set of fields that the requester can edit on the given content
"""
# Shared fields
ret = {"voted"}
if _is_author_or_privileged(cc_content, context):
ret |= {"raw_body"}
# Thread fields
if cc_content["type"] == "thread":
ret |= {"following"}
if _is_author_or_privileged(cc_content, context):
ret |= {"topic_id", "type", "title"}
# Comment fields
if (
cc_content["type"] == "comment" and (
context["is_requester_privileged"] or (
_is_author(context["thread"], context) and
context["thread"]["thread_type"] == "question"
)
)
):
ret |= {"endorsed"}
return ret
def can_delete(cc_content, context):
"""
Return True if the requester can delete the given content, False otherwise
"""
return _is_author_or_privileged(cc_content, context)
...@@ -10,6 +10,7 @@ from django.core.urlresolvers import reverse ...@@ -10,6 +10,7 @@ from django.core.urlresolvers import reverse
from rest_framework import serializers from rest_framework import serializers
from discussion_api.permissions import get_editable_fields
from discussion_api.render import render_body from discussion_api.render import render_body
from django_comment_common.models import ( from django_comment_common.models import (
FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_ADMINISTRATOR,
...@@ -70,6 +71,7 @@ class _ContentSerializer(serializers.Serializer): ...@@ -70,6 +71,7 @@ class _ContentSerializer(serializers.Serializer):
abuse_flagged = serializers.SerializerMethodField("get_abuse_flagged") abuse_flagged = serializers.SerializerMethodField("get_abuse_flagged")
voted = serializers.SerializerMethodField("get_voted") voted = serializers.SerializerMethodField("get_voted")
vote_count = serializers.SerializerMethodField("get_vote_count") vote_count = serializers.SerializerMethodField("get_vote_count")
editable_fields = serializers.SerializerMethodField("get_editable_fields")
non_updatable_fields = () non_updatable_fields = ()
...@@ -146,6 +148,10 @@ class _ContentSerializer(serializers.Serializer): ...@@ -146,6 +148,10 @@ class _ContentSerializer(serializers.Serializer):
"""Returns the number of votes for the content.""" """Returns the number of votes for the content."""
return obj["votes"]["up_count"] return obj["votes"]["up_count"]
def get_editable_fields(self, obj):
"""Return the list of the fields the requester can edit"""
return sorted(get_editable_fields(obj, self.context))
class ThreadSerializer(_ContentSerializer): class ThreadSerializer(_ContentSerializer):
""" """
......
...@@ -541,6 +541,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest ...@@ -541,6 +541,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
def test_thread_content(self): def test_thread_content(self):
source_threads = [ source_threads = [
{ {
"type": "thread",
"id": "test_thread_id_0", "id": "test_thread_id_0",
"course_id": unicode(self.course.id), "course_id": unicode(self.course.id),
"commentable_id": "topic_x", "commentable_id": "topic_x",
...@@ -562,6 +563,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest ...@@ -562,6 +563,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
"unread_comments_count": 3, "unread_comments_count": 3,
}, },
{ {
"type": "thread",
"id": "test_thread_id_1", "id": "test_thread_id_1",
"course_id": unicode(self.course.id), "course_id": unicode(self.course.id),
"commentable_id": "topic_y", "commentable_id": "topic_y",
...@@ -609,6 +611,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest ...@@ -609,6 +611,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
"comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread_id_0", "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread_id_0",
"endorsed_comment_list_url": None, "endorsed_comment_list_url": None,
"non_endorsed_comment_list_url": None, "non_endorsed_comment_list_url": None,
"editable_fields": ["following", "voted"],
}, },
{ {
"id": "test_thread_id_1", "id": "test_thread_id_1",
...@@ -639,6 +642,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest ...@@ -639,6 +642,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
"non_endorsed_comment_list_url": ( "non_endorsed_comment_list_url": (
"http://testserver/api/discussion/v1/comments/?thread_id=test_thread_id_1&endorsed=False" "http://testserver/api/discussion/v1/comments/?thread_id=test_thread_id_1&endorsed=False"
), ),
"editable_fields": ["following", "voted"],
}, },
] ]
self.assertEqual( self.assertEqual(
...@@ -915,6 +919,7 @@ class GetCommentListTest(CommentsServiceMockMixin, ModuleStoreTestCase): ...@@ -915,6 +919,7 @@ class GetCommentListTest(CommentsServiceMockMixin, ModuleStoreTestCase):
def test_discussion_content(self): def test_discussion_content(self):
source_comments = [ source_comments = [
{ {
"type": "comment",
"id": "test_comment_1", "id": "test_comment_1",
"thread_id": "test_thread", "thread_id": "test_thread",
"user_id": str(self.author.id), "user_id": str(self.author.id),
...@@ -930,6 +935,7 @@ class GetCommentListTest(CommentsServiceMockMixin, ModuleStoreTestCase): ...@@ -930,6 +935,7 @@ class GetCommentListTest(CommentsServiceMockMixin, ModuleStoreTestCase):
"children": [], "children": [],
}, },
{ {
"type": "comment",
"id": "test_comment_2", "id": "test_comment_2",
"thread_id": "test_thread", "thread_id": "test_thread",
"user_id": str(self.author.id), "user_id": str(self.author.id),
...@@ -964,6 +970,7 @@ class GetCommentListTest(CommentsServiceMockMixin, ModuleStoreTestCase): ...@@ -964,6 +970,7 @@ class GetCommentListTest(CommentsServiceMockMixin, ModuleStoreTestCase):
"voted": False, "voted": False,
"vote_count": 4, "vote_count": 4,
"children": [], "children": [],
"editable_fields": ["voted"],
}, },
{ {
"id": "test_comment_2", "id": "test_comment_2",
...@@ -983,6 +990,7 @@ class GetCommentListTest(CommentsServiceMockMixin, ModuleStoreTestCase): ...@@ -983,6 +990,7 @@ class GetCommentListTest(CommentsServiceMockMixin, ModuleStoreTestCase):
"voted": False, "voted": False,
"vote_count": 7, "vote_count": 7,
"children": [], "children": [],
"editable_fields": ["voted"],
}, },
] ]
actual_comments = self.get_comment_list( actual_comments = self.get_comment_list(
...@@ -1202,6 +1210,7 @@ class CreateThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestC ...@@ -1202,6 +1210,7 @@ class CreateThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestC
"comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_id", "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_id",
"endorsed_comment_list_url": None, "endorsed_comment_list_url": None,
"non_endorsed_comment_list_url": None, "non_endorsed_comment_list_url": None,
"editable_fields": ["following", "raw_body", "title", "topic_id", "type", "voted"],
} }
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
self.assertEqual( self.assertEqual(
...@@ -1367,6 +1376,7 @@ class CreateCommentTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest ...@@ -1367,6 +1376,7 @@ class CreateCommentTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
"voted": False, "voted": False,
"vote_count": 0, "vote_count": 0,
"children": [], "children": [],
"editable_fields": ["raw_body", "voted"]
} }
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
expected_url = ( expected_url = (
...@@ -1535,7 +1545,7 @@ class UpdateThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestC ...@@ -1535,7 +1545,7 @@ class UpdateThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestC
"user_id": str(self.user.id), "user_id": str(self.user.id),
"created_at": "2015-05-29T00:00:00Z", "created_at": "2015-05-29T00:00:00Z",
"updated_at": "2015-05-29T00:00:00Z", "updated_at": "2015-05-29T00:00:00Z",
"type": "discussion", "thread_type": "discussion",
"title": "Original Title", "title": "Original Title",
"body": "Original body", "body": "Original body",
}) })
...@@ -1580,6 +1590,7 @@ class UpdateThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestC ...@@ -1580,6 +1590,7 @@ class UpdateThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestC
"comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread", "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread",
"endorsed_comment_list_url": None, "endorsed_comment_list_url": None,
"non_endorsed_comment_list_url": None, "non_endorsed_comment_list_url": None,
"editable_fields": ["following", "raw_body", "title", "topic_id", "type", "voted"],
} }
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
self.assertEqual( self.assertEqual(
...@@ -1841,6 +1852,7 @@ class UpdateCommentTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest ...@@ -1841,6 +1852,7 @@ class UpdateCommentTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
"voted": False, "voted": False,
"vote_count": 0, "vote_count": 0,
"children": [], "children": [],
"editable_fields": ["raw_body", "voted"]
} }
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
self.assertEqual( self.assertEqual(
...@@ -1959,19 +1971,24 @@ class UpdateCommentTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest ...@@ -1959,19 +1971,24 @@ class UpdateCommentTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
FORUM_ROLE_STUDENT, FORUM_ROLE_STUDENT,
], ],
[True, False], [True, False],
["question", "discussion"],
[True, False], [True, False],
)) ))
@ddt.unpack @ddt.unpack
def test_endorsed_access(self, role_name, is_thread_author, is_comment_author): def test_endorsed_access(self, role_name, is_thread_author, thread_type, is_comment_author):
role = Role.objects.create(name=role_name, course_id=self.course.id) role = Role.objects.create(name=role_name, course_id=self.course.id)
role.users = [self.user] role.users = [self.user]
self.register_comment( self.register_comment(
{"user_id": str(self.user.id if is_comment_author else (self.user.id + 1))}, {"user_id": str(self.user.id if is_comment_author else (self.user.id + 1))},
thread_overrides={ thread_overrides={
"user_id": str(self.user.id if is_thread_author else (self.user.id + 1)) "thread_type": thread_type,
"user_id": str(self.user.id if is_thread_author else (self.user.id + 1)),
} }
) )
expected_error = role_name == FORUM_ROLE_STUDENT and not is_thread_author expected_error = (
role_name == FORUM_ROLE_STUDENT and
(thread_type == "discussion" or not is_thread_author)
)
try: try:
update_comment(self.request, "test_comment", {"endorsed": True}) update_comment(self.request, "test_comment", {"endorsed": True})
self.assertFalse(expected_error) self.assertFalse(expected_error)
......
"""
Tests for discussion API permission logic
"""
import itertools
from unittest import TestCase
import ddt
from discussion_api.permissions import can_delete, get_editable_fields
from lms.lib.comment_client.comment import Comment
from lms.lib.comment_client.thread import Thread
from lms.lib.comment_client.user import User
def _get_context(requester_id, is_requester_privileged, thread=None):
"""Return a context suitable for testing the permissions module"""
return {
"cc_requester": User(id=requester_id),
"is_requester_privileged": is_requester_privileged,
"thread": thread,
}
@ddt.ddt
class GetEditableFieldsTest(TestCase):
"""Tests for get_editable_fields"""
@ddt.data(*itertools.product([True, False], [True, False]))
@ddt.unpack
def test_thread(self, is_author, is_privileged):
thread = Thread(user_id="5" if is_author else "6", type="thread")
context = _get_context(requester_id="5", is_requester_privileged=is_privileged)
actual = get_editable_fields(thread, context)
expected = {"following", "voted"}
if is_author or is_privileged:
expected |= {"topic_id", "type", "title", "raw_body"}
self.assertEqual(actual, expected)
@ddt.data(*itertools.product([True, False], [True, False], ["question", "discussion"], [True, False]))
@ddt.unpack
def test_comment(self, is_author, is_thread_author, thread_type, is_privileged):
comment = Comment(user_id="5" if is_author else "6", type="comment")
context = _get_context(
requester_id="5",
is_requester_privileged=is_privileged,
thread=Thread(user_id="5" if is_thread_author else "6", thread_type=thread_type)
)
actual = get_editable_fields(comment, context)
expected = {"voted"}
if is_author or is_privileged:
expected |= {"raw_body"}
if (is_thread_author and thread_type == "question") or is_privileged:
expected |= {"endorsed"}
self.assertEqual(actual, expected)
@ddt.ddt
class CanDeleteTest(TestCase):
"""Tests for can_delete"""
@ddt.data(*itertools.product([True, False], [True, False]))
@ddt.unpack
def test_thread(self, is_author, is_privileged):
thread = Thread(user_id="5" if is_author else "6")
context = _get_context(requester_id="5", is_requester_privileged=is_privileged)
self.assertEqual(can_delete(thread, context), is_author or is_privileged)
@ddt.data(*itertools.product([True, False], [True, False], [True, False]))
@ddt.unpack
def test_comment(self, is_author, is_thread_author, is_privileged):
comment = Comment(user_id="5" if is_author else "6")
context = _get_context(
requester_id="5",
is_requester_privileged=is_privileged,
thread=Thread(user_id="5" if is_thread_author else "6")
)
self.assertEqual(can_delete(comment, context), is_author or is_privileged)
...@@ -151,6 +151,7 @@ class ThreadSerializerSerializationTest(SerializerTestMixin, ModuleStoreTestCase ...@@ -151,6 +151,7 @@ class ThreadSerializerSerializationTest(SerializerTestMixin, ModuleStoreTestCase
def test_basic(self): def test_basic(self):
thread = { thread = {
"type": "thread",
"id": "test_thread", "id": "test_thread",
"course_id": unicode(self.course.id), "course_id": unicode(self.course.id),
"commentable_id": "test_topic", "commentable_id": "test_topic",
...@@ -196,6 +197,7 @@ class ThreadSerializerSerializationTest(SerializerTestMixin, ModuleStoreTestCase ...@@ -196,6 +197,7 @@ class ThreadSerializerSerializationTest(SerializerTestMixin, ModuleStoreTestCase
"comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread", "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread",
"endorsed_comment_list_url": None, "endorsed_comment_list_url": None,
"non_endorsed_comment_list_url": None, "non_endorsed_comment_list_url": None,
"editable_fields": ["following", "voted"],
} }
self.assertEqual(self.serialize(thread), expected) self.assertEqual(self.serialize(thread), expected)
...@@ -259,6 +261,7 @@ class CommentSerializerTest(SerializerTestMixin, ModuleStoreTestCase): ...@@ -259,6 +261,7 @@ class CommentSerializerTest(SerializerTestMixin, ModuleStoreTestCase):
def test_basic(self): def test_basic(self):
comment = { comment = {
"type": "comment",
"id": "test_comment", "id": "test_comment",
"thread_id": "test_thread", "thread_id": "test_thread",
"user_id": str(self.author.id), "user_id": str(self.author.id),
...@@ -291,6 +294,7 @@ class CommentSerializerTest(SerializerTestMixin, ModuleStoreTestCase): ...@@ -291,6 +294,7 @@ class CommentSerializerTest(SerializerTestMixin, ModuleStoreTestCase):
"voted": False, "voted": False,
"vote_count": 4, "vote_count": 4,
"children": [], "children": [],
"editable_fields": ["voted"],
} }
self.assertEqual(self.serialize(comment), expected) self.assertEqual(self.serialize(comment), expected)
......
...@@ -158,6 +158,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): ...@@ -158,6 +158,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
def test_basic(self): def test_basic(self):
self.register_get_user_response(self.user, upvoted_ids=["test_thread"]) self.register_get_user_response(self.user, upvoted_ids=["test_thread"])
source_threads = [{ source_threads = [{
"type": "thread",
"id": "test_thread", "id": "test_thread",
"course_id": unicode(self.course.id), "course_id": unicode(self.course.id),
"commentable_id": "test_topic", "commentable_id": "test_topic",
...@@ -203,6 +204,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): ...@@ -203,6 +204,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread", "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread",
"endorsed_comment_list_url": None, "endorsed_comment_list_url": None,
"non_endorsed_comment_list_url": None, "non_endorsed_comment_list_url": None,
"editable_fields": ["following", "voted"],
}] }]
self.register_get_threads_response(source_threads, page=1, num_pages=2) self.register_get_threads_response(source_threads, page=1, num_pages=2)
response = self.client.get(self.url, {"course_id": unicode(self.course.id)}) response = self.client.get(self.url, {"course_id": unicode(self.course.id)})
...@@ -316,6 +318,7 @@ class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): ...@@ -316,6 +318,7 @@ class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread", "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread",
"endorsed_comment_list_url": None, "endorsed_comment_list_url": None,
"non_endorsed_comment_list_url": None, "non_endorsed_comment_list_url": None,
"editable_fields": ["following", "raw_body", "title", "topic_id", "type", "voted"],
} }
response = self.client.post( response = self.client.post(
self.url, self.url,
...@@ -406,6 +409,7 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest ...@@ -406,6 +409,7 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest
"comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread", "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread",
"endorsed_comment_list_url": None, "endorsed_comment_list_url": None,
"non_endorsed_comment_list_url": None, "non_endorsed_comment_list_url": None,
"editable_fields": ["following", "raw_body", "title", "topic_id", "type", "voted"],
} }
response = self.client.patch( # pylint: disable=no-member response = self.client.patch( # pylint: disable=no-member
self.url, self.url,
...@@ -515,6 +519,7 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): ...@@ -515,6 +519,7 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
def test_basic(self): def test_basic(self):
self.register_get_user_response(self.user, upvoted_ids=["test_comment"]) self.register_get_user_response(self.user, upvoted_ids=["test_comment"])
source_comments = [{ source_comments = [{
"type": "comment",
"id": "test_comment", "id": "test_comment",
"thread_id": self.thread_id, "thread_id": self.thread_id,
"parent_id": None, "parent_id": None,
...@@ -548,6 +553,7 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): ...@@ -548,6 +553,7 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"voted": True, "voted": True,
"vote_count": 4, "vote_count": 4,
"children": [], "children": [],
"editable_fields": ["voted"],
}] }]
self.register_get_thread_response({ self.register_get_thread_response({
"id": self.thread_id, "id": self.thread_id,
...@@ -701,6 +707,7 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): ...@@ -701,6 +707,7 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"voted": False, "voted": False,
"vote_count": 0, "vote_count": 0,
"children": [], "children": [],
"editable_fields": ["raw_body", "voted"],
} }
response = self.client.post( response = self.client.post(
self.url, self.url,
...@@ -784,6 +791,7 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes ...@@ -784,6 +791,7 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes
"voted": False, "voted": False,
"vote_count": 0, "vote_count": 0,
"children": [], "children": [],
"editable_fields": ["raw_body", "voted"],
} }
response = self.client.patch( # pylint: disable=no-member response = self.client.patch( # pylint: disable=no-member
self.url, self.url,
......
...@@ -273,6 +273,7 @@ def make_minimal_cs_thread(overrides=None): ...@@ -273,6 +273,7 @@ def make_minimal_cs_thread(overrides=None):
comments service with dummy data and optional overrides comments service with dummy data and optional overrides
""" """
ret = { ret = {
"type": "thread",
"id": "dummy", "id": "dummy",
"course_id": "dummy/dummy/dummy", "course_id": "dummy/dummy/dummy",
"commentable_id": "dummy", "commentable_id": "dummy",
...@@ -305,6 +306,7 @@ def make_minimal_cs_comment(overrides=None): ...@@ -305,6 +306,7 @@ def make_minimal_cs_comment(overrides=None):
comments service with dummy data and optional overrides comments service with dummy data and optional overrides
""" """
ret = { ret = {
"type": "comment",
"id": "dummy", "id": "dummy",
"thread_id": "dummy", "thread_id": "dummy",
"parent_id": None, "parent_id": None,
......
...@@ -203,6 +203,9 @@ class ThreadViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet): ...@@ -203,6 +203,9 @@ class ThreadViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
that were created or updated since the last time the user read that were created or updated since the last time the user read
the thread the thread
* editable_fields: The fields that the requesting user is allowed to
modify with a PATCH request
**DELETE response values: **DELETE response values:
No content is returned for a DELETE request No content is returned for a DELETE request
...@@ -352,6 +355,9 @@ class CommentViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet): ...@@ -352,6 +355,9 @@ class CommentViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
* children: The list of child comments (with the same format) * children: The list of child comments (with the same format)
* editable_fields: The fields that the requesting user is allowed to
modify with a PATCH request
**DELETE Response Value** **DELETE Response Value**
No content is returned for a DELETE request No content is returned for a DELETE request
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment