From eabd6e8019adbd3949e7a137b314145378924813 Mon Sep 17 00:00:00 2001 From: Maxim Beder <30300520+Cup0fCoffee@users.noreply.github.com> Date: Tue, 2 Nov 2021 07:25:40 +0100 Subject: [PATCH] feat: add anonymous settings to discussions api (#28981) If course allows anonymous posts, and user is an author of a post, add `anonymous` to posts' editable fields. If course allows anonymous to peers posts, and user is an author of a post, add `anonymous_to_peers` to posts' editable fields. Course endpoint response to get request will now include course's `allow_anonymous` and `allow_anonymous_to_peers` settings. Added `anonymous` and `anonymous_to_peers` fields to the content serializer, from which ThreadSerializer and CommentSerializer inherit. Both fields have a default value of False, because there are cases where course configuration doesn't allow them to be initialized (if a course doesn't allow anonymous posts, the fields won't be included in the list of initializable/editable fields). --- lms/djangoapps/discussion/rest_api/api.py | 4 +- .../discussion/rest_api/permissions.py | 4 +- .../discussion/rest_api/serializers.py | 2 + .../discussion/rest_api/tests/test_api.py | 38 ++++-- .../rest_api/tests/test_permissions.py | 74 ++++++++++-- .../rest_api/tests/test_serializers.py | 114 +++++++++++++++++- .../discussion/rest_api/tests/test_views.py | 36 ++++-- .../discussion/rest_api/tests/utils.py | 7 +- lms/djangoapps/discussion/rest_api/views.py | 35 +++++- 9 files changed, 274 insertions(+), 40 deletions(-) diff --git a/lms/djangoapps/discussion/rest_api/api.py b/lms/djangoapps/discussion/rest_api/api.py index 64f612f6a2..862c0178d8 100644 --- a/lms/djangoapps/discussion/rest_api/api.py +++ b/lms/djangoapps/discussion/rest_api/api.py @@ -260,7 +260,9 @@ def get_course(request, course_key): "following_thread_list_url": get_thread_list_url(request, course_key, following=True), "topics_url": request.build_absolute_uri( reverse("course_topics", kwargs={"course_id": course_key}) - ) + ), + "allow_anonymous": course.allow_anonymous, + "allow_anonymous_to_peers": course.allow_anonymous_to_peers, } diff --git a/lms/djangoapps/discussion/rest_api/permissions.py b/lms/djangoapps/discussion/rest_api/permissions.py index bca338700c..1346a4f76b 100644 --- a/lms/djangoapps/discussion/rest_api/permissions.py +++ b/lms/djangoapps/discussion/rest_api/permissions.py @@ -105,7 +105,9 @@ def get_editable_fields(cc_content: Union[Thread, Comment], context: Dict) -> Se is_comment and (is_privileged or (_is_author(context["thread"], context) and context["thread"]["thread_type"] == "question")) - ) + ), + "anonymous": is_author and context["course"].allow_anonymous, + "anonymous_to_peers": is_author and context["course"].allow_anonymous_to_peers, }) # Return only editable fields return _filter_fields(editable_fields) diff --git a/lms/djangoapps/discussion/rest_api/serializers.py b/lms/djangoapps/discussion/rest_api/serializers.py index c5a0ddbd78..662c622b50 100644 --- a/lms/djangoapps/discussion/rest_api/serializers.py +++ b/lms/djangoapps/discussion/rest_api/serializers.py @@ -122,6 +122,8 @@ class _ContentSerializer(serializers.Serializer): vote_count = serializers.SerializerMethodField() editable_fields = serializers.SerializerMethodField() can_delete = serializers.SerializerMethodField() + anonymous = serializers.BooleanField(default=False) + anonymous_to_peers = serializers.BooleanField(default=False) non_updatable_fields = set() diff --git a/lms/djangoapps/discussion/rest_api/tests/test_api.py b/lms/djangoapps/discussion/rest_api/tests/test_api.py index d106b05f53..bc2644fa8e 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_api.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_api.py @@ -176,7 +176,9 @@ class GetCourseTest(ForumsEnableMixin, UrlResetMixin, SharedModuleStoreTestCase) 'thread_list_url': 'http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz', 'following_thread_list_url': 'http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz&following=True', - 'topics_url': 'http://testserver/api/discussion/v1/course_topics/x/y/z' + 'topics_url': 'http://testserver/api/discussion/v1/course_topics/x/y/z', + 'allow_anonymous': True, + 'allow_anonymous_to_peers': False, } @@ -1379,6 +1381,8 @@ class GetCommentListTest(ForumsEnableMixin, CommentsServiceMockMixin, SharedModu "child_count": 0, "children": [], "can_delete": False, + "anonymous": False, + "anonymous_to_peers": False, }, { "id": "test_comment_2", @@ -1402,6 +1406,8 @@ class GetCommentListTest(ForumsEnableMixin, CommentsServiceMockMixin, SharedModu "child_count": 0, "children": [], "can_delete": False, + "anonymous": True, + "anonymous_to_peers": False, }, ] actual_comments = self.get_comment_list( @@ -1659,7 +1665,9 @@ class CreateThreadTest( 'thread_type': ['discussion'], 'title': ['Test Title'], 'body': ['Test body'], - 'user_id': [str(self.user.id)] + 'user_id': [str(self.user.id)], + 'anonymous': ['False'], + 'anonymous_to_peers': ['False'], } event_name, event_data = mock_emit.call_args[0] assert event_name == 'edx.forum.thread.created' @@ -1716,8 +1724,8 @@ class CreateThreadTest( "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_id", "read": True, "editable_fields": [ - "abuse_flagged", "closed", "following", "pinned", "raw_body", "read", "title", "topic_id", "type", - "voted" + "abuse_flagged", "anonymous", "closed", "following", "pinned", + "raw_body", "read", "title", "topic_id", "type", "voted" ], }) assert actual == expected @@ -1730,6 +1738,8 @@ class CreateThreadTest( "title": ["Test Title"], "body": ["Test body"], "user_id": [str(self.user.id)], + "anonymous": ["False"], + "anonymous_to_peers": ["False"], } ) event_name, event_data = mock_emit.call_args[0] @@ -1999,9 +2009,11 @@ class CreateCommentTest( "voted": False, "vote_count": 0, "children": [], - "editable_fields": ["abuse_flagged", "raw_body", "voted"], + "editable_fields": ["abuse_flagged", "anonymous", "raw_body", "voted"], "child_count": 0, "can_delete": True, + "anonymous": False, + "anonymous_to_peers": False, } assert actual == expected expected_url = ( @@ -2012,7 +2024,9 @@ class CreateCommentTest( assert httpretty.last_request().parsed_body == { # lint-amnesty, pylint: disable=no-member 'course_id': [str(self.course.id)], 'body': ['Test body'], - 'user_id': [str(self.user.id)] + 'user_id': [str(self.user.id)], + 'anonymous': ['False'], + 'anonymous_to_peers': ['False'], } expected_event_name = ( "edx.forum.comment.created" if parent_id else @@ -2081,9 +2095,11 @@ class CreateCommentTest( "voted": False, "vote_count": 0, "children": [], - "editable_fields": ["abuse_flagged", "endorsed", "raw_body", "voted"], + "editable_fields": ["abuse_flagged", "anonymous", "endorsed", "raw_body", "voted"], "child_count": 0, "can_delete": True, + "anonymous": False, + "anonymous_to_peers": False, } assert actual == expected expected_url = ( @@ -2094,7 +2110,9 @@ class CreateCommentTest( assert httpretty.last_request().parsed_body == { # pylint: disable=no-member "course_id": [str(self.course.id)], "body": ["Test body"], - "user_id": [str(self.user.id)] + "user_id": [str(self.user.id)], + "anonymous": ['False'], + "anonymous_to_peers": ['False'], } expected_event_name = ( @@ -2698,6 +2716,8 @@ class UpdateCommentTest( with self.assert_signal_sent(api, 'comment_edited', sender=None, user=self.user, exclude_args=('post',)): actual = update_comment(self.request, "test_comment", {"raw_body": "Edited body"}) expected = { + "anonymous": False, + "anonymous_to_peers": False, "id": "test_comment", "thread_id": "test_thread", "parent_id": parent_id, @@ -2716,7 +2736,7 @@ class UpdateCommentTest( "voted": False, "vote_count": 0, "children": [], - "editable_fields": ["abuse_flagged", "raw_body", "voted"], + "editable_fields": ["abuse_flagged", "anonymous", "raw_body", "voted"], "child_count": 0, "can_delete": True, } diff --git a/lms/djangoapps/discussion/rest_api/tests/test_permissions.py b/lms/djangoapps/discussion/rest_api/tests/test_permissions.py index 57c419c360..210ad09046 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_permissions.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_permissions.py @@ -20,12 +20,23 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory -def _get_context(requester_id, is_requester_privileged, is_cohorted=False, thread=None): +def _get_context( + requester_id, + is_requester_privileged, + is_cohorted=False, + thread=None, + allow_anonymous=True, + allow_anonymous_to_peers=False, +): """Return a context suitable for testing the permissions module""" return { "cc_requester": User(id=requester_id), "is_requester_privileged": is_requester_privileged, - "course": CourseFactory(cohort_config={"cohorted": is_cohorted}), + "course": CourseFactory( + cohort_config={"cohorted": is_cohorted}, + allow_anonymous=allow_anonymous, + allow_anonymous_to_peers=allow_anonymous_to_peers, + ), "discussion_division_enabled": is_cohorted, "thread": thread, } @@ -34,13 +45,21 @@ def _get_context(requester_id, is_requester_privileged, is_cohorted=False, threa @ddt.ddt class GetInitializableFieldsTest(ModuleStoreTestCase): """Tests for get_*_initializable_fields""" - @ddt.data(*itertools.product([True, False], [True, False])) + @ddt.data(*itertools.product(*[[True, False] for _ in range(4)])) @ddt.unpack - def test_thread(self, is_privileged, is_cohorted): + def test_thread( + self, + is_privileged, + is_cohorted, + allow_anonymous, + allow_anonymous_to_peers, + ): context = _get_context( requester_id="5", is_requester_privileged=is_privileged, - is_cohorted=is_cohorted + is_cohorted=is_cohorted, + allow_anonymous=allow_anonymous, + allow_anonymous_to_peers=allow_anonymous_to_peers, ) actual = get_initializable_thread_fields(context) expected = { @@ -50,6 +69,10 @@ class GetInitializableFieldsTest(ModuleStoreTestCase): expected |= {"closed", "pinned"} if is_privileged and is_cohorted: expected |= {"group_id"} + if allow_anonymous: + expected |= {"anonymous"} + if allow_anonymous_to_peers: + expected |= {"anonymous_to_peers"} assert actual == expected @ddt.data(*itertools.product([True, False], ["question", "discussion"], [True, False])) @@ -62,7 +85,7 @@ class GetInitializableFieldsTest(ModuleStoreTestCase): ) actual = get_initializable_comment_fields(context) expected = { - "abuse_flagged", "parent_id", "raw_body", "thread_id", "voted" + "anonymous", "abuse_flagged", "parent_id", "raw_body", "thread_id", "voted" } if (is_thread_author and thread_type == "question") or is_privileged: expected |= {"endorsed"} @@ -72,14 +95,23 @@ class GetInitializableFieldsTest(ModuleStoreTestCase): @ddt.ddt class GetEditableFieldsTest(ModuleStoreTestCase): """Tests for get_editable_fields""" - @ddt.data(*itertools.product([True, False], [True, False], [True, False])) + @ddt.data(*itertools.product(*[[True, False] for _ in range(5)])) @ddt.unpack - def test_thread(self, is_author, is_privileged, is_cohorted): + def test_thread( + self, + is_author, + is_privileged, + is_cohorted, + allow_anonymous, + allow_anonymous_to_peers + ): thread = Thread(user_id="5" if is_author else "6", type="thread") context = _get_context( requester_id="5", is_requester_privileged=is_privileged, - is_cohorted=is_cohorted + is_cohorted=is_cohorted, + allow_anonymous=allow_anonymous, + allow_anonymous_to_peers=allow_anonymous_to_peers, ) actual = get_editable_fields(thread, context) expected = {"abuse_flagged", "following", "read", "voted"} @@ -89,16 +121,30 @@ class GetEditableFieldsTest(ModuleStoreTestCase): expected |= {"topic_id", "type", "title", "raw_body"} if is_privileged and is_cohorted: expected |= {"group_id"} + if is_author and allow_anonymous: + expected |= {"anonymous"} + if is_author and allow_anonymous_to_peers: + expected |= {"anonymous_to_peers"} assert actual == expected - @ddt.data(*itertools.product([True, False], [True, False], ["question", "discussion"], [True, False])) + @ddt.data(*itertools.product(*[[True, False] for _ in range(5)], ["question", "discussion"])) @ddt.unpack - def test_comment(self, is_author, is_thread_author, thread_type, is_privileged): + def test_comment( + self, + is_author, + is_thread_author, + is_privileged, + allow_anonymous, + allow_anonymous_to_peers, + thread_type + ): 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) + thread=Thread(user_id="5" if is_thread_author else "6", thread_type=thread_type), + allow_anonymous=allow_anonymous, + allow_anonymous_to_peers=allow_anonymous_to_peers, ) actual = get_editable_fields(comment, context) expected = {"abuse_flagged", "voted"} @@ -106,6 +152,10 @@ class GetEditableFieldsTest(ModuleStoreTestCase): expected |= {"raw_body"} if (is_thread_author and thread_type == "question") or is_privileged: expected |= {"endorsed"} + if is_author and allow_anonymous: + expected |= {"anonymous"} + if is_author and allow_anonymous_to_peers: + expected |= {"anonymous_to_peers"} assert actual == expected diff --git a/lms/djangoapps/discussion/rest_api/tests/test_serializers.py b/lms/djangoapps/discussion/rest_api/tests/test_serializers.py index 888cac0f26..2e462177ab 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_serializers.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_serializers.py @@ -293,6 +293,8 @@ class CommentSerializerTest(SerializerTestMixin, SharedModuleStoreTestCase): "child_count": 0, } expected = { + "anonymous": False, + "anonymous_to_peers": False, "id": "test_comment", "thread_id": "test_thread", "parent_id": None, @@ -476,7 +478,9 @@ class ThreadSerializerDeserializationTest( 'thread_type': ['discussion'], 'title': ['Test Title'], 'body': ['Test body'], - 'user_id': [str(self.user.id)] + 'user_id': [str(self.user.id)], + 'anonymous': ['False'], + 'anonymous_to_peers': ['False'], } assert saved['id'] == 'test_id' @@ -492,7 +496,9 @@ class ThreadSerializerDeserializationTest( 'title': ['Test Title'], 'body': ['Test body'], 'user_id': [str(self.user.id)], - 'group_id': ['42'] + 'group_id': ['42'], + 'anonymous': ['False'], + 'anonymous_to_peers': ['False'], } def test_create_missing_field(self): @@ -523,6 +529,28 @@ class ThreadSerializerDeserializationTest( serializer = ThreadSerializer(data=data) assert not serializer.is_valid() + def test_create_anonymous(self): + """ + Test that serializer correctly deserializes the anonymous field when + creating a new thread. + """ + self.register_post_thread_response({"id": "test_id", "username": self.user.username}) + data = self.minimal_data.copy() + data["anonymous"] = True + self.save_and_reserialize(data) + assert httpretty.last_request().parsed_body["anonymous"] == ['True'] # lint-amnesty, pylint: disable=no-member + + def test_create_anonymous_to_peers(self): + """ + Test that serializer correctly deserializes the anonymous_to_peers field + when creating a new thread. + """ + self.register_post_thread_response({"id": "test_id", "username": self.user.username}) + data = self.minimal_data.copy() + data["anonymous_to_peers"] = True + self.save_and_reserialize(data) + assert httpretty.last_request().parsed_body["anonymous_to_peers"] == ['True'] # lint-amnesty, pylint: disable=no-member + def test_update_empty(self): self.register_put_thread_response(self.existing_thread.attributes) self.save_and_reserialize({}, self.existing_thread) @@ -567,6 +595,30 @@ class ThreadSerializerDeserializationTest( for key in data: assert saved[key] == data[key] + def test_update_anonymous(self): + """ + Test that serializer correctly deserializes the anonymous field when + updating an existing thread. + """ + self.register_put_thread_response(self.existing_thread.attributes) + data = { + "anonymous": True, + } + self.save_and_reserialize(data, self.existing_thread) + assert httpretty.last_request().parsed_body["anonymous"] == ['True'] # lint-amnesty, pylint: disable=no-member + + def test_update_anonymous_to_peers(self): + """ + Test that serializer correctly deserializes the anonymous_to_peers + field when updating an existing thread. + """ + self.register_put_thread_response(self.existing_thread.attributes) + data = { + "anonymous_to_peers": True, + } + self.save_and_reserialize(data, self.existing_thread) + assert httpretty.last_request().parsed_body["anonymous_to_peers"] == ['True'] # lint-amnesty, pylint: disable=no-member + @ddt.data("", " ") def test_update_empty_string(self, value): serializer = ThreadSerializer( @@ -662,7 +714,9 @@ class CommentSerializerDeserializationTest(ForumsEnableMixin, CommentsServiceMoc assert httpretty.last_request().parsed_body == { # lint-amnesty, pylint: disable=no-member 'course_id': [str(self.course.id)], 'body': ['Test body'], - 'user_id': [str(self.user.id)] + 'user_id': [str(self.user.id)], + 'anonymous': ['False'], + 'anonymous_to_peers': ['False'], } assert saved['id'] == 'test_comment' assert saved['parent_id'] == parent_id @@ -682,7 +736,9 @@ class CommentSerializerDeserializationTest(ForumsEnableMixin, CommentsServiceMoc 'course_id': [str(self.course.id)], 'body': ['Test body'], 'user_id': [str(self.user.id)], - 'endorsed': ['True'] + 'endorsed': ['True'], + 'anonymous': ['False'], + 'anonymous_to_peers': ['False'], } def test_create_parent_id_nonexistent(self): @@ -760,13 +816,37 @@ class CommentSerializerDeserializationTest(ForumsEnableMixin, CommentsServiceMoc 'course_id': [str(self.course.id)], 'body': ['Test body'], 'user_id': [str(self.user.id)], - 'endorsed': ['True'] + 'endorsed': ['True'], + 'anonymous': ['False'], + 'anonymous_to_peers': ['False'], } assert saved['endorsed'] assert saved['endorsed_by'] is None assert saved['endorsed_by_label'] is None assert saved['endorsed_at'] is None + def test_create_anonymous(self): + """ + Test that serializer correctly deserializes the anonymous field when + creating a new comment. + """ + self.register_post_comment_response({"username": self.user.username}, thread_id="test_thread") + data = self.minimal_data.copy() + data["anonymous"] = True + self.save_and_reserialize(data) + assert httpretty.last_request().parsed_body["anonymous"] == ['True'] # lint-amnesty, pylint: disable=no-member + + def test_create_anonymous_to_peers(self): + """ + Test that serializer correctly deserializes the anonymous_to_peers + field when creating a new comment. + """ + self.register_post_comment_response({"username": self.user.username}, thread_id="test_thread") + data = self.minimal_data.copy() + data["anonymous_to_peers"] = True + self.save_and_reserialize(data) + assert httpretty.last_request().parsed_body["anonymous_to_peers"] == ['True'] # lint-amnesty, pylint: disable=no-member + def test_update_empty(self): self.register_put_comment_response(self.existing_comment.attributes) self.save_and_reserialize({}, instance=self.existing_comment) @@ -813,6 +893,30 @@ class CommentSerializerDeserializationTest(ForumsEnableMixin, CommentsServiceMoc assert not serializer.is_valid() assert serializer.errors == {'raw_body': ['This field may not be blank.']} + def test_update_anonymous(self): + """ + Test that serializer correctly deserializes the anonymous field when + updating an existing comment. + """ + self.register_put_comment_response(self.existing_comment.attributes) + data = { + "anonymous": True, + } + self.save_and_reserialize(data, self.existing_comment) + assert httpretty.last_request().parsed_body["anonymous"] == ['True'] # lint-amnesty, pylint: disable=no-member + + def test_update_anonymous_to_peers(self): + """ + Test that serializer correctly deserializes the anonymous_to_peers + field when updating an existing comment. + """ + self.register_put_comment_response(self.existing_comment.attributes) + data = { + "anonymous_to_peers": True, + } + self.save_and_reserialize(data, self.existing_comment) + assert httpretty.last_request().parsed_body["anonymous_to_peers"] == ['True'] # lint-amnesty, pylint: disable=no-member + @ddt.data("thread_id", "parent_id") def test_update_non_updatable(self, field): serializer = CommentSerializer( diff --git a/lms/djangoapps/discussion/rest_api/tests/test_views.py b/lms/djangoapps/discussion/rest_api/tests/test_views.py index f8d848e49f..b54b1235a1 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_views.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_views.py @@ -163,6 +163,8 @@ class CourseViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): "http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz&following=True" ), "topics_url": "http://testserver/api/discussion/v1/course_topics/x/y/z", + "allow_anonymous": True, + "allow_anonymous_to_peers": False, } ) @@ -887,8 +889,10 @@ class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): 'commentable_id': ['test_topic'], 'thread_type': ['discussion'], 'title': ['Test Title'], - "body": ["# Test \n This is a very long body that will be truncated for the preview."], - 'user_id': [str(self.user.id)] + 'body': ['# Test \n This is a very long body that will be truncated for the preview.'], + 'user_id': [str(self.user.id)], + 'anonymous': ['False'], + 'anonymous_to_peers': ['False'], } def test_error(self): @@ -938,7 +942,10 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest 'raw_body': 'Edited body', 'rendered_body': '
Edited body
', 'preview_body': 'Edited body', - 'editable_fields': ['abuse_flagged', 'following', 'raw_body', 'read', 'title', 'topic_id', 'type', 'voted'], + 'editable_fields': [ + 'abuse_flagged', 'anonymous', 'following', 'raw_body', 'read', + 'title', 'topic_id', 'type', 'voted' + ], 'created_at': 'Test Created Date', 'updated_at': 'Test Updated Date', 'comment_count': 1, @@ -1018,7 +1025,10 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest assert response_data == self.expected_thread_data({ 'comment_count': 1, 'read': True, - 'editable_fields': ['abuse_flagged', 'following', 'raw_body', 'read', 'title', 'topic_id', 'type', 'voted'], + 'editable_fields': [ + 'abuse_flagged', 'anonymous', 'following', 'raw_body', 'read', + 'title', 'topic_id', 'type', 'voted' + ], 'response_count': 2 }) @@ -1145,6 +1155,8 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pr "editable_fields": ["abuse_flagged", "voted"], "child_count": 0, "can_delete": True, + "anonymous": False, + "anonymous_to_peers": False, } response_data.update(overrides or {}) return response_data @@ -1532,9 +1544,11 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): "voted": False, "vote_count": 0, "children": [], - "editable_fields": ["abuse_flagged", "raw_body", "voted"], + "editable_fields": ["abuse_flagged", "anonymous", "raw_body", "voted"], "child_count": 0, "can_delete": True, + "anonymous": False, + "anonymous_to_peers": False, } response = self.client.post( self.url, @@ -1548,7 +1562,9 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): assert httpretty.last_request().parsed_body == { # lint-amnesty, pylint: disable=no-member 'course_id': [str(self.course.id)], 'body': ['Test body'], - 'user_id': [str(self.user.id)] + 'user_id': [str(self.user.id)], + 'anonymous': ['False'], + 'anonymous_to_peers': ['False'], } def test_error(self): @@ -1621,6 +1637,8 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes "editable_fields": [], "child_count": 0, "can_delete": True, + "anonymous": False, + "anonymous_to_peers": False, } response_data.update(overrides or {}) return response_data @@ -1635,7 +1653,7 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes assert response_data == self.expected_response_data({ 'raw_body': 'Edited body', 'rendered_body': 'Edited body
', - 'editable_fields': ['abuse_flagged', 'raw_body', 'voted'], + 'editable_fields': ['abuse_flagged', 'anonymous', 'raw_body', 'voted'], 'created_at': 'Test Created Date', 'updated_at': 'Test Updated Date' }) @@ -1803,9 +1821,11 @@ class CommentViewSetRetrieveTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase "vote_count": 0, "abuse_flagged": False, "abuse_flagged_any_user": None, - "editable_fields": ["abuse_flagged", "raw_body", "voted"], + "editable_fields": ["abuse_flagged", "anonymous", "raw_body", "voted"], "child_count": 0, "can_delete": True, + "anonymous": False, + "anonymous_to_peers": False, } response = self.client.get(self.url) diff --git a/lms/djangoapps/discussion/rest_api/tests/utils.py b/lms/djangoapps/discussion/rest_api/tests/utils.py index 29ed62b91f..091139331b 100644 --- a/lms/djangoapps/discussion/rest_api/tests/utils.py +++ b/lms/djangoapps/discussion/rest_api/tests/utils.py @@ -392,6 +392,8 @@ class CommentsServiceMockMixin: Returns expected thread data in API response """ response_data = { + "anonymous": False, + "anonymous_to_peers": False, "author": self.user.username, "author_label": None, "created_at": "1970-01-01T00:00:00Z", @@ -403,7 +405,10 @@ class CommentsServiceMockMixin: "abuse_flagged_count": None, "voted": False, "vote_count": 0, - "editable_fields": ["abuse_flagged", "following", "raw_body", "read", "title", "topic_id", "type", "voted"], + "editable_fields": [ + "abuse_flagged", "anonymous", "following", "raw_body", "read", + "title", "topic_id", "type", "voted" + ], "course_id": str(self.course.id), "topic_id": "test_topic", "group_id": None, diff --git a/lms/djangoapps/discussion/rest_api/views.py b/lms/djangoapps/discussion/rest_api/views.py index 9fb446a937..e040a7c2af 100644 --- a/lms/djangoapps/discussion/rest_api/views.py +++ b/lms/djangoapps/discussion/rest_api/views.py @@ -87,6 +87,12 @@ class CourseView(DeveloperErrorViewMixin, APIView): * following_thread_list_url: thread_list_url with parameter following=True * topics_url: The URL of the topic listing for the course. + + * allow_anonymous: A boolean which indicating whether anonymous posts + are allowed or not. + + * allow_anonymous_to_peers: A boolean which indicating whether posts + anonymous to peers are allowed or not. """ def get(self, request, course_id): """Implements the GET method as described in the class docstring.""" @@ -239,6 +245,12 @@ class ThreadViewSet(DeveloperErrorViewMixin, ViewSet): * following (optional): A boolean indicating whether the user should follow the thread upon its creation; defaults to false + * anonymous (optional): A boolean indicating whether the post is + anonymous; defaults to false + + * anonymous_to_peers (optional): A boolean indicating whether the post + is anonymous to peers; defaults to false + **PATCH Parameters**: * abuse_flagged (optional): A boolean to mark thread as abusive @@ -247,8 +259,8 @@ class ThreadViewSet(DeveloperErrorViewMixin, ViewSet): * read (optional): A boolean to mark thread as read - * topic_id, type, title, and raw_body are accepted with the same meaning - as in a POST request + * topic_id, type, title, raw_body, anonymous, and anonymous_to_peers + are accepted with the same meaning as in a POST request If "application/merge-patch+json" is not the specified content type, a 415 error is returned. @@ -311,6 +323,11 @@ class ThreadViewSet(DeveloperErrorViewMixin, ViewSet): * abuse_flagged_count: The number of flags(reports) on and within the thread. Returns null if requesting user is not a moderator + * anonymous: A boolean indicating whether the post is anonymous + + * anonymous_to_peers: A boolean indicating whether the post is + anonymous to peers + **DELETE response values: No content is returned for a DELETE request @@ -446,9 +463,16 @@ class CommentViewSet(DeveloperErrorViewMixin, ViewSet): * raw_body: The comment's raw body text + * anonymous (optional): A boolean indicating whether the comment is + anonymous; defaults to false + + * anonymous_to_peers (optional): A boolean indicating whether the + comment is anonymous to peers; defaults to false + **PATCH Parameters**: - raw_body is accepted with the same meaning as in a POST request + * raw_body, anonymous and anonymous_to_peers are accepted with the same + meaning as in a POST request If "application/merge-patch+json" is not the specified content type, a 415 error is returned. @@ -513,6 +537,11 @@ class CommentViewSet(DeveloperErrorViewMixin, ViewSet): * editable_fields: The fields that the requesting user is allowed to modify with a PATCH request + * anonymous: A boolean indicating whether the comment is anonymous + + * anonymous_to_peers: A boolean indicating whether the comment is + anonymous to peers + **DELETE Response Value** No content is returned for a DELETE request