1830 lines
69 KiB
Python
1830 lines
69 KiB
Python
"""
|
|
Tests for Discussion API views
|
|
"""
|
|
from datetime import datetime
|
|
import json
|
|
from urlparse import urlparse
|
|
|
|
import ddt
|
|
import httpretty
|
|
import mock
|
|
from nose.plugins.attrib import attr
|
|
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_storage
|
|
from pytz import UTC
|
|
|
|
from django.core.urlresolvers import reverse
|
|
from rest_framework.parsers import JSONParser
|
|
|
|
from rest_framework.test import APIClient
|
|
from xmodule.modulestore import ModuleStoreEnum
|
|
from xmodule.modulestore.django import modulestore
|
|
|
|
from common.test.utils import disable_signal
|
|
from discussion_api import api
|
|
from discussion_api.tests.utils import (
|
|
CommentsServiceMockMixin,
|
|
make_minimal_cs_comment,
|
|
make_minimal_cs_thread,
|
|
make_paginated_api_response,
|
|
ProfileImageTestMixin)
|
|
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
|
from util.testing import UrlResetMixin, PatchMediaTypeMixin
|
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
|
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls, ItemFactory
|
|
|
|
|
|
class DiscussionAPIViewTestMixin(CommentsServiceMockMixin, UrlResetMixin):
|
|
"""
|
|
Mixin for common code in tests of Discussion API views. This includes
|
|
creation of common structures (e.g. a course, user, and enrollment), logging
|
|
in the test client, utility functions, and a test case for unauthenticated
|
|
requests. Subclasses must set self.url in their setUp methods.
|
|
"""
|
|
client_class = APIClient
|
|
|
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
def setUp(self):
|
|
super(DiscussionAPIViewTestMixin, self).setUp()
|
|
self.maxDiff = None # pylint: disable=invalid-name
|
|
self.course = CourseFactory.create(
|
|
org="x",
|
|
course="y",
|
|
run="z",
|
|
start=datetime.now(UTC),
|
|
discussion_topics={"Test Topic": {"id": "test_topic"}}
|
|
)
|
|
self.password = "password"
|
|
self.user = UserFactory.create(password=self.password)
|
|
# Ensure that parental controls don't apply to this user
|
|
self.user.profile.year_of_birth = 1970
|
|
self.user.profile.save()
|
|
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
|
|
self.client.login(username=self.user.username, password=self.password)
|
|
|
|
def assert_response_correct(self, response, expected_status, expected_content):
|
|
"""
|
|
Assert that the response has the given status code and parsed content
|
|
"""
|
|
self.assertEqual(response.status_code, expected_status)
|
|
parsed_content = json.loads(response.content)
|
|
self.assertEqual(parsed_content, expected_content)
|
|
|
|
def register_thread(self, overrides=None):
|
|
"""
|
|
Create cs_thread with minimal fields and register response
|
|
"""
|
|
cs_thread = make_minimal_cs_thread({
|
|
"id": "test_thread",
|
|
"course_id": unicode(self.course.id),
|
|
"commentable_id": "original_topic",
|
|
"username": self.user.username,
|
|
"user_id": str(self.user.id),
|
|
"thread_type": "discussion",
|
|
"title": "Original Title",
|
|
"body": "Original body",
|
|
})
|
|
cs_thread.update(overrides or {})
|
|
self.register_get_thread_response(cs_thread)
|
|
self.register_put_thread_response(cs_thread)
|
|
|
|
def register_comment(self, overrides=None):
|
|
"""
|
|
Create cs_comment with minimal fields and register response
|
|
"""
|
|
cs_comment = make_minimal_cs_comment({
|
|
"id": "test_comment",
|
|
"course_id": unicode(self.course.id),
|
|
"thread_id": "test_thread",
|
|
"username": self.user.username,
|
|
"user_id": str(self.user.id),
|
|
"body": "Original body",
|
|
})
|
|
cs_comment.update(overrides or {})
|
|
self.register_get_comment_response(cs_comment)
|
|
self.register_put_comment_response(cs_comment)
|
|
self.register_post_comment_response(cs_comment, thread_id="test_thread")
|
|
|
|
def test_not_authenticated(self):
|
|
self.client.logout()
|
|
response = self.client.get(self.url)
|
|
self.assert_response_correct(
|
|
response,
|
|
401,
|
|
{"developer_message": "Authentication credentials were not provided."}
|
|
)
|
|
|
|
def test_inactive(self):
|
|
self.user.is_active = False
|
|
self.test_basic()
|
|
|
|
|
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
class CourseViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
|
|
"""Tests for CourseView"""
|
|
def setUp(self):
|
|
super(CourseViewTest, self).setUp()
|
|
self.url = reverse("discussion_course", kwargs={"course_id": unicode(self.course.id)})
|
|
|
|
def test_404(self):
|
|
response = self.client.get(
|
|
reverse("course_topics", kwargs={"course_id": "non/existent/course"})
|
|
)
|
|
self.assert_response_correct(
|
|
response,
|
|
404,
|
|
{"developer_message": "Course not found."}
|
|
)
|
|
|
|
def test_basic(self):
|
|
response = self.client.get(self.url)
|
|
self.assert_response_correct(
|
|
response,
|
|
200,
|
|
{
|
|
"id": unicode(self.course.id),
|
|
"blackouts": [],
|
|
"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",
|
|
}
|
|
)
|
|
|
|
|
|
@ddt.ddt
|
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
class CourseTopicsViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
|
|
"""
|
|
Tests for CourseTopicsView
|
|
"""
|
|
def setUp(self):
|
|
super(CourseTopicsViewTest, self).setUp()
|
|
self.url = reverse("course_topics", kwargs={"course_id": unicode(self.course.id)})
|
|
|
|
def create_course(self, modules_count, module_store, topics):
|
|
"""
|
|
Create a course in a specified module store with discussion xblocks and topics
|
|
"""
|
|
course = CourseFactory.create(
|
|
org="a",
|
|
course="b",
|
|
run="c",
|
|
start=datetime.now(UTC),
|
|
default_store=module_store,
|
|
discussion_topics=topics
|
|
)
|
|
CourseEnrollmentFactory.create(user=self.user, course_id=course.id)
|
|
course_url = reverse("course_topics", kwargs={"course_id": unicode(course.id)})
|
|
# add some discussion xblocks
|
|
for i in range(modules_count):
|
|
ItemFactory.create(
|
|
parent_location=course.location,
|
|
category='discussion',
|
|
discussion_id='id_module_{}'.format(i),
|
|
discussion_category='Category {}'.format(i),
|
|
discussion_target='Discussion {}'.format(i),
|
|
publish_item=False,
|
|
)
|
|
return course_url
|
|
|
|
def make_discussion_xblock(self, topic_id, category, subcategory, **kwargs):
|
|
"""
|
|
Build a discussion xblock in self.course
|
|
"""
|
|
ItemFactory.create(
|
|
parent_location=self.course.location,
|
|
category="discussion",
|
|
discussion_id=topic_id,
|
|
discussion_category=category,
|
|
discussion_target=subcategory,
|
|
**kwargs
|
|
)
|
|
|
|
def test_404(self):
|
|
response = self.client.get(
|
|
reverse("course_topics", kwargs={"course_id": "non/existent/course"})
|
|
)
|
|
self.assert_response_correct(
|
|
response,
|
|
404,
|
|
{"developer_message": "Course not found."}
|
|
)
|
|
|
|
def test_basic(self):
|
|
response = self.client.get(self.url)
|
|
self.assert_response_correct(
|
|
response,
|
|
200,
|
|
{
|
|
"courseware_topics": [],
|
|
"non_courseware_topics": [{
|
|
"id": "test_topic",
|
|
"name": "Test Topic",
|
|
"children": [],
|
|
"thread_list_url":
|
|
"http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz&topic_id=test_topic",
|
|
}],
|
|
}
|
|
)
|
|
|
|
@ddt.data(
|
|
(2, ModuleStoreEnum.Type.mongo, 2, {"Test Topic 1": {"id": "test_topic_1"}}),
|
|
(2, ModuleStoreEnum.Type.mongo, 2,
|
|
{"Test Topic 1": {"id": "test_topic_1"}, "Test Topic 2": {"id": "test_topic_2"}}),
|
|
(2, ModuleStoreEnum.Type.split, 3, {"Test Topic 1": {"id": "test_topic_1"}}),
|
|
(2, ModuleStoreEnum.Type.split, 3,
|
|
{"Test Topic 1": {"id": "test_topic_1"}, "Test Topic 2": {"id": "test_topic_2"}}),
|
|
(10, ModuleStoreEnum.Type.split, 3, {"Test Topic 1": {"id": "test_topic_1"}}),
|
|
)
|
|
@ddt.unpack
|
|
def test_bulk_response(self, modules_count, module_store, mongo_calls, topics):
|
|
course_url = self.create_course(modules_count, module_store, topics)
|
|
with check_mongo_calls(mongo_calls):
|
|
with modulestore().default_store(module_store):
|
|
self.client.get(course_url)
|
|
|
|
def test_discussion_topic_404(self):
|
|
"""
|
|
Tests discussion topic does not exist for the given topic id.
|
|
"""
|
|
topic_id = "courseware-topic-id"
|
|
self.make_discussion_xblock(topic_id, "test_category", "test_target")
|
|
url = "{}?topic_id=invalid_topic_id".format(self.url)
|
|
response = self.client.get(url)
|
|
self.assert_response_correct(
|
|
response,
|
|
404,
|
|
{"developer_message": "Discussion not found for 'invalid_topic_id'."}
|
|
)
|
|
|
|
def test_topic_id(self):
|
|
"""
|
|
Tests discussion topic details against a requested topic id
|
|
"""
|
|
topic_id_1 = "topic_id_1"
|
|
topic_id_2 = "topic_id_2"
|
|
self.make_discussion_xblock(topic_id_1, "test_category_1", "test_target_1")
|
|
self.make_discussion_xblock(topic_id_2, "test_category_2", "test_target_2")
|
|
url = "{}?topic_id=topic_id_1,topic_id_2".format(self.url)
|
|
response = self.client.get(url)
|
|
self.assert_response_correct(
|
|
response,
|
|
200,
|
|
{
|
|
"non_courseware_topics": [],
|
|
"courseware_topics": [
|
|
{
|
|
"children": [{
|
|
"children": [],
|
|
"id": "topic_id_1",
|
|
"thread_list_url": "http://testserver/api/discussion/v1/threads/?"
|
|
"course_id=x%2Fy%2Fz&topic_id=topic_id_1",
|
|
"name": "test_target_1"
|
|
}],
|
|
"id": None,
|
|
"thread_list_url": "http://testserver/api/discussion/v1/threads/?"
|
|
"course_id=x%2Fy%2Fz&topic_id=topic_id_1",
|
|
"name": "test_category_1"
|
|
},
|
|
{
|
|
"children":
|
|
[{
|
|
"children": [],
|
|
"id": "topic_id_2",
|
|
"thread_list_url": "http://testserver/api/discussion/v1/threads/?"
|
|
"course_id=x%2Fy%2Fz&topic_id=topic_id_2",
|
|
"name": "test_target_2"
|
|
}],
|
|
"id": None,
|
|
"thread_list_url": "http://testserver/api/discussion/v1/threads/?"
|
|
"course_id=x%2Fy%2Fz&topic_id=topic_id_2",
|
|
"name": "test_category_2"
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
|
|
@attr(shard=3)
|
|
@ddt.ddt
|
|
@httpretty.activate
|
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, ProfileImageTestMixin):
|
|
"""Tests for ThreadViewSet list"""
|
|
def setUp(self):
|
|
super(ThreadViewSetListTest, self).setUp()
|
|
self.author = UserFactory.create()
|
|
self.url = reverse("thread-list")
|
|
|
|
def make_expected_thread(self, overrides=None):
|
|
"""
|
|
Create a sample expected thread for response
|
|
"""
|
|
thread = {
|
|
"id": "test_thread",
|
|
"course_id": unicode(self.course.id),
|
|
"topic_id": "test_topic",
|
|
"group_id": None,
|
|
"group_name": None,
|
|
"author": "dummy",
|
|
"author_label": None,
|
|
"created_at": "1970-01-01T00:00:00Z",
|
|
"updated_at": "1970-01-01T00:00:00Z",
|
|
"type": "discussion",
|
|
"title": "dummy",
|
|
"raw_body": "dummy",
|
|
"rendered_body": "<p>dummy</p>",
|
|
"pinned": False,
|
|
"closed": False,
|
|
"following": False,
|
|
"abuse_flagged": False,
|
|
"voted": False,
|
|
"vote_count": 0,
|
|
"comment_count": 1,
|
|
"unread_comment_count": 1,
|
|
"comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread",
|
|
"endorsed_comment_list_url": None,
|
|
"non_endorsed_comment_list_url": None,
|
|
"editable_fields": ["abuse_flagged", "following", "read", "voted"],
|
|
"read": False,
|
|
"has_endorsed": False,
|
|
"response_count": 0,
|
|
}
|
|
thread.update(overrides or {})
|
|
return thread
|
|
|
|
def create_source_thread(self, overrides=None):
|
|
"""
|
|
Create a sample source cs_thread
|
|
"""
|
|
thread = make_minimal_cs_thread({
|
|
"id": "test_thread",
|
|
"course_id": unicode(self.course.id),
|
|
"commentable_id": "test_topic",
|
|
"user_id": str(self.user.id),
|
|
"username": self.user.username,
|
|
"created_at": "2015-04-28T00:00:00Z",
|
|
"updated_at": "2015-04-28T11:11:11Z",
|
|
"title": "Test Title",
|
|
"body": "Test body",
|
|
"votes": {"up_count": 4},
|
|
"comments_count": 5,
|
|
"unread_comments_count": 3,
|
|
})
|
|
|
|
thread.update(overrides or {})
|
|
return thread
|
|
|
|
def test_course_id_missing(self):
|
|
response = self.client.get(self.url)
|
|
self.assert_response_correct(
|
|
response,
|
|
400,
|
|
{"field_errors": {"course_id": {"developer_message": "This field is required."}}}
|
|
)
|
|
|
|
def test_404(self):
|
|
response = self.client.get(self.url, {"course_id": unicode("non/existent/course")})
|
|
self.assert_response_correct(
|
|
response,
|
|
404,
|
|
{"developer_message": "Course not found."}
|
|
)
|
|
|
|
def test_basic(self):
|
|
self.register_get_user_response(self.user, upvoted_ids=["test_thread"])
|
|
source_threads = [
|
|
self.create_source_thread({"user_id": str(self.author.id), "username": self.author.username})
|
|
]
|
|
expected_threads = [self.make_expected_thread({
|
|
"created_at": "2015-04-28T00:00:00Z",
|
|
"updated_at": "2015-04-28T11:11:11Z",
|
|
"raw_body": "Test body",
|
|
"rendered_body": "<p>Test body</p>",
|
|
"title": "Test Title",
|
|
"vote_count": 4,
|
|
"comment_count": 6,
|
|
"unread_comment_count": 4,
|
|
"voted": True,
|
|
"author": self.author.username
|
|
})]
|
|
self.register_get_threads_response(source_threads, page=1, num_pages=2)
|
|
response = self.client.get(self.url, {"course_id": unicode(self.course.id), "following": ""})
|
|
expected_response = make_paginated_api_response(
|
|
results=expected_threads,
|
|
count=1,
|
|
num_pages=2,
|
|
next_link="http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz&page=2",
|
|
previous_link=None
|
|
)
|
|
expected_response.update({"text_search_rewrite": None})
|
|
self.assert_response_correct(
|
|
response,
|
|
200,
|
|
expected_response
|
|
)
|
|
self.assert_last_query_params({
|
|
"user_id": [unicode(self.user.id)],
|
|
"course_id": [unicode(self.course.id)],
|
|
"sort_key": ["activity"],
|
|
"page": ["1"],
|
|
"per_page": ["10"],
|
|
})
|
|
|
|
@ddt.data("unread", "unanswered")
|
|
def test_view_query(self, query):
|
|
threads = [make_minimal_cs_thread()]
|
|
self.register_get_user_response(self.user)
|
|
self.register_get_threads_response(threads, page=1, num_pages=1)
|
|
self.client.get(
|
|
self.url,
|
|
{
|
|
"course_id": unicode(self.course.id),
|
|
"view": query,
|
|
}
|
|
)
|
|
self.assert_last_query_params({
|
|
"user_id": [unicode(self.user.id)],
|
|
"course_id": [unicode(self.course.id)],
|
|
"sort_key": ["activity"],
|
|
"page": ["1"],
|
|
"per_page": ["10"],
|
|
query: ["true"],
|
|
})
|
|
|
|
def test_pagination(self):
|
|
self.register_get_user_response(self.user)
|
|
self.register_get_threads_response([], page=1, num_pages=1)
|
|
response = self.client.get(
|
|
self.url,
|
|
{"course_id": unicode(self.course.id), "page": "18", "page_size": "4"}
|
|
)
|
|
self.assert_response_correct(
|
|
response,
|
|
404,
|
|
{"developer_message": "Page not found (No results on this page)."}
|
|
)
|
|
self.assert_last_query_params({
|
|
"user_id": [unicode(self.user.id)],
|
|
"course_id": [unicode(self.course.id)],
|
|
"sort_key": ["activity"],
|
|
"page": ["18"],
|
|
"per_page": ["4"],
|
|
})
|
|
|
|
def test_text_search(self):
|
|
self.register_get_user_response(self.user)
|
|
self.register_get_threads_search_response([], None, num_pages=0)
|
|
response = self.client.get(
|
|
self.url,
|
|
{"course_id": unicode(self.course.id), "text_search": "test search string"}
|
|
)
|
|
|
|
expected_response = make_paginated_api_response(
|
|
results=[], count=0, num_pages=0, next_link=None, previous_link=None
|
|
)
|
|
expected_response.update({"text_search_rewrite": None})
|
|
self.assert_response_correct(
|
|
response,
|
|
200,
|
|
expected_response
|
|
)
|
|
self.assert_last_query_params({
|
|
"user_id": [unicode(self.user.id)],
|
|
"course_id": [unicode(self.course.id)],
|
|
"sort_key": ["activity"],
|
|
"page": ["1"],
|
|
"per_page": ["10"],
|
|
"text": ["test search string"],
|
|
})
|
|
|
|
@ddt.data(True, "true", "1")
|
|
def test_following_true(self, following):
|
|
self.register_get_user_response(self.user)
|
|
self.register_subscribed_threads_response(self.user, [], page=1, num_pages=0)
|
|
response = self.client.get(
|
|
self.url,
|
|
{
|
|
"course_id": unicode(self.course.id),
|
|
"following": following,
|
|
}
|
|
)
|
|
|
|
expected_response = make_paginated_api_response(
|
|
results=[], count=0, num_pages=0, next_link=None, previous_link=None
|
|
)
|
|
expected_response.update({"text_search_rewrite": None})
|
|
self.assert_response_correct(
|
|
response,
|
|
200,
|
|
expected_response
|
|
)
|
|
self.assertEqual(
|
|
urlparse(httpretty.last_request().path).path,
|
|
"/api/v1/users/{}/subscribed_threads".format(self.user.id)
|
|
)
|
|
|
|
@ddt.data(False, "false", "0")
|
|
def test_following_false(self, following):
|
|
response = self.client.get(
|
|
self.url,
|
|
{
|
|
"course_id": unicode(self.course.id),
|
|
"following": following,
|
|
}
|
|
)
|
|
self.assert_response_correct(
|
|
response,
|
|
400,
|
|
{"field_errors": {
|
|
"following": {"developer_message": "The value of the 'following' parameter must be true."}
|
|
}}
|
|
)
|
|
|
|
def test_following_error(self):
|
|
response = self.client.get(
|
|
self.url,
|
|
{
|
|
"course_id": unicode(self.course.id),
|
|
"following": "invalid-boolean",
|
|
}
|
|
)
|
|
self.assert_response_correct(
|
|
response,
|
|
400,
|
|
{"field_errors": {
|
|
"following": {"developer_message": "Invalid Boolean Value."}
|
|
}}
|
|
)
|
|
|
|
@ddt.data(
|
|
("last_activity_at", "activity"),
|
|
("comment_count", "comments"),
|
|
("vote_count", "votes")
|
|
)
|
|
@ddt.unpack
|
|
def test_order_by(self, http_query, cc_query):
|
|
"""
|
|
Tests the order_by parameter
|
|
|
|
Arguments:
|
|
http_query (str): Query string sent in the http request
|
|
cc_query (str): Query string used for the comments client service
|
|
"""
|
|
threads = [make_minimal_cs_thread()]
|
|
self.register_get_user_response(self.user)
|
|
self.register_get_threads_response(threads, page=1, num_pages=1)
|
|
self.client.get(
|
|
self.url,
|
|
{
|
|
"course_id": unicode(self.course.id),
|
|
"order_by": http_query,
|
|
}
|
|
)
|
|
self.assert_last_query_params({
|
|
"user_id": [unicode(self.user.id)],
|
|
"course_id": [unicode(self.course.id)],
|
|
"page": ["1"],
|
|
"per_page": ["10"],
|
|
"sort_key": [cc_query],
|
|
})
|
|
|
|
def test_order_direction(self):
|
|
"""
|
|
Test order direction, of which "desc" is the only valid option. The
|
|
option actually just gets swallowed, so it doesn't affect the params.
|
|
"""
|
|
threads = [make_minimal_cs_thread()]
|
|
self.register_get_user_response(self.user)
|
|
self.register_get_threads_response(threads, page=1, num_pages=1)
|
|
self.client.get(
|
|
self.url,
|
|
{
|
|
"course_id": unicode(self.course.id),
|
|
"order_direction": "desc",
|
|
}
|
|
)
|
|
self.assert_last_query_params({
|
|
"user_id": [unicode(self.user.id)],
|
|
"course_id": [unicode(self.course.id)],
|
|
"sort_key": ["activity"],
|
|
"page": ["1"],
|
|
"per_page": ["10"],
|
|
})
|
|
|
|
def test_mutually_exclusive(self):
|
|
"""
|
|
Tests GET thread_list api does not allow filtering on mutually exclusive parameters
|
|
"""
|
|
self.register_get_user_response(self.user)
|
|
self.register_get_threads_search_response([], None, num_pages=0)
|
|
response = self.client.get(self.url, {
|
|
"course_id": unicode(self.course.id),
|
|
"text_search": "test search string",
|
|
"topic_id": "topic1, topic2",
|
|
})
|
|
self.assert_response_correct(
|
|
response,
|
|
400,
|
|
{
|
|
"developer_message": "The following query parameters are mutually exclusive: topic_id, "
|
|
"text_search, following"
|
|
}
|
|
)
|
|
|
|
def test_profile_image_requested_field(self):
|
|
"""
|
|
Tests thread has user profile image details if called in requested_fields
|
|
"""
|
|
user_2 = UserFactory.create(password=self.password)
|
|
# Ensure that parental controls don't apply to this user
|
|
user_2.profile.year_of_birth = 1970
|
|
user_2.profile.save()
|
|
source_threads = [
|
|
self.create_source_thread(),
|
|
self.create_source_thread({"user_id": str(user_2.id), "username": user_2.username}),
|
|
]
|
|
|
|
self.register_get_user_response(self.user, upvoted_ids=["test_thread"])
|
|
self.register_get_threads_response(source_threads, page=1, num_pages=1)
|
|
self.create_profile_image(self.user, get_profile_image_storage())
|
|
self.create_profile_image(user_2, get_profile_image_storage())
|
|
|
|
response = self.client.get(
|
|
self.url,
|
|
{"course_id": unicode(self.course.id), "requested_fields": "profile_image"},
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
response_threads = json.loads(response.content)['results']
|
|
|
|
for response_thread in response_threads:
|
|
expected_profile_data = self.get_expected_user_profile(response_thread['author'])
|
|
response_users = response_thread['users']
|
|
self.assertEqual(expected_profile_data, response_users[response_thread['author']])
|
|
|
|
def test_profile_image_requested_field_anonymous_user(self):
|
|
"""
|
|
Tests profile_image in requested_fields for thread created with anonymous user
|
|
"""
|
|
source_threads = [
|
|
self.create_source_thread(
|
|
{"user_id": None, "username": None, "anonymous": True, "anonymous_to_peers": True}
|
|
),
|
|
]
|
|
|
|
self.register_get_user_response(self.user, upvoted_ids=["test_thread"])
|
|
self.register_get_threads_response(source_threads, page=1, num_pages=1)
|
|
|
|
response = self.client.get(
|
|
self.url,
|
|
{"course_id": unicode(self.course.id), "requested_fields": "profile_image"},
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
response_thread = json.loads(response.content)['results'][0]
|
|
self.assertIsNone(response_thread['author'])
|
|
self.assertEqual({}, response_thread['users'])
|
|
|
|
|
|
@httpretty.activate
|
|
@disable_signal(api, 'thread_created')
|
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
|
|
"""Tests for ThreadViewSet create"""
|
|
def setUp(self):
|
|
super(ThreadViewSetCreateTest, self).setUp()
|
|
self.url = reverse("thread-list")
|
|
|
|
def test_basic(self):
|
|
self.register_get_user_response(self.user)
|
|
cs_thread = make_minimal_cs_thread({
|
|
"id": "test_thread",
|
|
"username": self.user.username,
|
|
"created_at": "2015-05-19T00:00:00Z",
|
|
"updated_at": "2015-05-19T00:00:00Z",
|
|
"read": True,
|
|
})
|
|
self.register_post_thread_response(cs_thread)
|
|
request_data = {
|
|
"course_id": unicode(self.course.id),
|
|
"topic_id": "test_topic",
|
|
"type": "discussion",
|
|
"title": "Test Title",
|
|
"raw_body": "Test body",
|
|
}
|
|
expected_response_data = {
|
|
"id": "test_thread",
|
|
"course_id": unicode(self.course.id),
|
|
"topic_id": "test_topic",
|
|
"group_id": None,
|
|
"group_name": None,
|
|
"author": self.user.username,
|
|
"author_label": None,
|
|
"created_at": "2015-05-19T00:00:00Z",
|
|
"updated_at": "2015-05-19T00:00:00Z",
|
|
"type": "discussion",
|
|
"title": "Test Title",
|
|
"raw_body": "Test body",
|
|
"rendered_body": "<p>Test body</p>",
|
|
"pinned": False,
|
|
"closed": False,
|
|
"following": False,
|
|
"abuse_flagged": False,
|
|
"voted": False,
|
|
"vote_count": 0,
|
|
"comment_count": 1,
|
|
"unread_comment_count": 0,
|
|
"comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread",
|
|
"endorsed_comment_list_url": None,
|
|
"non_endorsed_comment_list_url": None,
|
|
"editable_fields": ["abuse_flagged", "following", "raw_body", "read", "title", "topic_id", "type", "voted"],
|
|
"read": True,
|
|
"has_endorsed": False,
|
|
"response_count": 0,
|
|
}
|
|
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(
|
|
httpretty.last_request().parsed_body,
|
|
{
|
|
"course_id": [unicode(self.course.id)],
|
|
"commentable_id": ["test_topic"],
|
|
"thread_type": ["discussion"],
|
|
"title": ["Test Title"],
|
|
"body": ["Test body"],
|
|
"user_id": [str(self.user.id)],
|
|
}
|
|
)
|
|
|
|
def test_error(self):
|
|
request_data = {
|
|
"topic_id": "dummy",
|
|
"type": "discussion",
|
|
"title": "dummy",
|
|
"raw_body": "dummy",
|
|
}
|
|
response = self.client.post(
|
|
self.url,
|
|
json.dumps(request_data),
|
|
content_type="application/json"
|
|
)
|
|
expected_response_data = {
|
|
"field_errors": {"course_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)
|
|
|
|
|
|
@attr(shard=3)
|
|
@ddt.ddt
|
|
@httpretty.activate
|
|
@disable_signal(api, 'thread_edited')
|
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, PatchMediaTypeMixin):
|
|
"""Tests for ThreadViewSet partial_update"""
|
|
def setUp(self):
|
|
self.unsupported_media_type = JSONParser.media_type
|
|
super(ThreadViewSetPartialUpdateTest, self).setUp()
|
|
self.url = reverse("thread-detail", kwargs={"thread_id": "test_thread"})
|
|
|
|
def expected_response_data(self, overrides=None):
|
|
"""
|
|
create expected response data from comment update endpoint
|
|
"""
|
|
response_data = {
|
|
"id": "test_thread",
|
|
"course_id": unicode(self.course.id),
|
|
"topic_id": "original_topic",
|
|
"group_id": None,
|
|
"group_name": None,
|
|
"author": self.user.username,
|
|
"author_label": None,
|
|
"created_at": "1970-01-01T00:00:00Z",
|
|
"updated_at": "1970-01-01T00:00:00Z",
|
|
"type": "discussion",
|
|
"title": "Original Title",
|
|
"raw_body": "Original body",
|
|
"rendered_body": "<p>Original body</p>",
|
|
"pinned": False,
|
|
"closed": False,
|
|
"following": False,
|
|
"abuse_flagged": False,
|
|
"voted": False,
|
|
"vote_count": 0,
|
|
"comment_count": 0,
|
|
"unread_comment_count": 0,
|
|
"comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread",
|
|
"endorsed_comment_list_url": None,
|
|
"non_endorsed_comment_list_url": None,
|
|
"editable_fields": [],
|
|
"read": False,
|
|
"has_endorsed": False,
|
|
"response_count": 0,
|
|
}
|
|
response_data.update(overrides or {})
|
|
return response_data
|
|
|
|
def test_basic(self):
|
|
self.register_get_user_response(self.user)
|
|
self.register_thread({"created_at": "Test Created Date", "updated_at": "Test Updated Date", "read": True})
|
|
request_data = {"raw_body": "Edited body"}
|
|
response = self.request_patch(request_data)
|
|
self.assertEqual(response.status_code, 200)
|
|
response_data = json.loads(response.content)
|
|
self.assertEqual(
|
|
response_data,
|
|
self.expected_response_data({
|
|
"raw_body": "Edited body",
|
|
"rendered_body": "<p>Edited body</p>",
|
|
"editable_fields": [
|
|
"abuse_flagged", "following", "raw_body", "read", "title", "topic_id", "type", "voted"
|
|
],
|
|
"created_at": "Test Created Date",
|
|
"updated_at": "Test Updated Date",
|
|
"comment_count": 1,
|
|
"read": True,
|
|
})
|
|
)
|
|
self.assertEqual(
|
|
httpretty.last_request().parsed_body,
|
|
{
|
|
"course_id": [unicode(self.course.id)],
|
|
"commentable_id": ["original_topic"],
|
|
"thread_type": ["discussion"],
|
|
"title": ["Original Title"],
|
|
"body": ["Edited body"],
|
|
"user_id": [str(self.user.id)],
|
|
"anonymous": ["False"],
|
|
"anonymous_to_peers": ["False"],
|
|
"closed": ["False"],
|
|
"pinned": ["False"],
|
|
"read": ["True"],
|
|
}
|
|
)
|
|
|
|
def test_error(self):
|
|
self.register_get_user_response(self.user)
|
|
self.register_thread()
|
|
request_data = {"title": ""}
|
|
response = self.request_patch(request_data)
|
|
expected_response_data = {
|
|
"field_errors": {"title": {"developer_message": "This field may not be blank."}}
|
|
}
|
|
self.assertEqual(response.status_code, 400)
|
|
response_data = json.loads(response.content)
|
|
self.assertEqual(response_data, expected_response_data)
|
|
|
|
@ddt.data(
|
|
("abuse_flagged", True),
|
|
("abuse_flagged", False),
|
|
)
|
|
@ddt.unpack
|
|
def test_closed_thread(self, field, value):
|
|
self.register_get_user_response(self.user)
|
|
self.register_thread({"closed": True, "read": True})
|
|
self.register_flag_response("thread", "test_thread")
|
|
request_data = {field: value}
|
|
response = self.request_patch(request_data)
|
|
self.assertEqual(response.status_code, 200)
|
|
response_data = json.loads(response.content)
|
|
self.assertEqual(
|
|
response_data,
|
|
self.expected_response_data({
|
|
"read": True,
|
|
"closed": True,
|
|
"abuse_flagged": value,
|
|
"editable_fields": ["abuse_flagged", "read"],
|
|
"comment_count": 1,
|
|
"unread_comment_count": 0,
|
|
})
|
|
)
|
|
|
|
@ddt.data(
|
|
("raw_body", "Edited body"),
|
|
("voted", True),
|
|
("following", True),
|
|
)
|
|
@ddt.unpack
|
|
def test_closed_thread_error(self, field, value):
|
|
self.register_get_user_response(self.user)
|
|
self.register_thread({"closed": True})
|
|
self.register_flag_response("thread", "test_thread")
|
|
request_data = {field: value}
|
|
response = self.request_patch(request_data)
|
|
self.assertEqual(response.status_code, 400)
|
|
|
|
def test_patch_read_owner_user(self):
|
|
self.register_get_user_response(self.user)
|
|
self.register_thread()
|
|
self.register_read_response(self.user, "thread", "test_thread")
|
|
request_data = {"read": True}
|
|
|
|
response = self.request_patch(request_data)
|
|
self.assertEqual(response.status_code, 200)
|
|
response_data = json.loads(response.content)
|
|
self.assertEqual(
|
|
response_data,
|
|
self.expected_response_data({
|
|
"comment_count": 1,
|
|
"read": True,
|
|
"editable_fields": [
|
|
"abuse_flagged", "following", "raw_body", "read", "title", "topic_id", "type", "voted"
|
|
],
|
|
})
|
|
)
|
|
|
|
def test_patch_read_non_owner_user(self):
|
|
self.register_get_user_response(self.user)
|
|
thread_owner_user = UserFactory.create(password=self.password)
|
|
CourseEnrollmentFactory.create(user=thread_owner_user, course_id=self.course.id)
|
|
self.register_get_user_response(thread_owner_user)
|
|
self.register_thread({"username": thread_owner_user.username, "user_id": str(thread_owner_user.id)})
|
|
self.register_read_response(self.user, "thread", "test_thread")
|
|
|
|
request_data = {"read": True}
|
|
response = self.request_patch(request_data)
|
|
self.assertEqual(response.status_code, 200)
|
|
response_data = json.loads(response.content)
|
|
self.assertEqual(
|
|
response_data,
|
|
self.expected_response_data({
|
|
"author": str(thread_owner_user.username),
|
|
"comment_count": 1,
|
|
"read": True,
|
|
"editable_fields": [
|
|
"abuse_flagged", "following", "read", "voted"
|
|
],
|
|
})
|
|
)
|
|
|
|
|
|
@httpretty.activate
|
|
@disable_signal(api, 'thread_deleted')
|
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
class ThreadViewSetDeleteTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
|
|
"""Tests for ThreadViewSet delete"""
|
|
def setUp(self):
|
|
super(ThreadViewSetDeleteTest, self).setUp()
|
|
self.url = reverse("thread-detail", kwargs={"thread_id": "test_thread"})
|
|
self.thread_id = "test_thread"
|
|
|
|
def test_basic(self):
|
|
self.register_get_user_response(self.user)
|
|
cs_thread = make_minimal_cs_thread({
|
|
"id": self.thread_id,
|
|
"course_id": unicode(self.course.id),
|
|
"username": self.user.username,
|
|
"user_id": str(self.user.id),
|
|
})
|
|
self.register_get_thread_response(cs_thread)
|
|
self.register_delete_thread_response(self.thread_id)
|
|
response = self.client.delete(self.url)
|
|
self.assertEqual(response.status_code, 204)
|
|
self.assertEqual(response.content, "")
|
|
self.assertEqual(
|
|
urlparse(httpretty.last_request().path).path,
|
|
"/api/v1/threads/{}".format(self.thread_id)
|
|
)
|
|
self.assertEqual(httpretty.last_request().method, "DELETE")
|
|
|
|
def test_delete_nonexistent_thread(self):
|
|
self.register_get_thread_error_response(self.thread_id, 404)
|
|
response = self.client.delete(self.url)
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
|
|
@attr(shard=3)
|
|
@ddt.ddt
|
|
@httpretty.activate
|
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, ProfileImageTestMixin):
|
|
"""Tests for CommentViewSet list"""
|
|
def setUp(self):
|
|
super(CommentViewSetListTest, self).setUp()
|
|
self.author = UserFactory.create()
|
|
self.url = reverse("comment-list")
|
|
self.thread_id = "test_thread"
|
|
self.storage = get_profile_image_storage()
|
|
|
|
def create_source_comment(self, overrides=None):
|
|
"""
|
|
Create a sample source cs_comment
|
|
"""
|
|
comment = make_minimal_cs_comment({
|
|
"id": "test_comment",
|
|
"thread_id": self.thread_id,
|
|
"user_id": str(self.user.id),
|
|
"username": self.user.username,
|
|
"created_at": "2015-05-11T00:00:00Z",
|
|
"updated_at": "2015-05-11T11:11:11Z",
|
|
"body": "Test body",
|
|
"votes": {"up_count": 4},
|
|
})
|
|
|
|
comment.update(overrides or {})
|
|
return comment
|
|
|
|
def make_minimal_cs_thread(self, overrides=None):
|
|
"""
|
|
Create a thread with the given overrides, plus the course_id if not
|
|
already in overrides.
|
|
"""
|
|
overrides = overrides.copy() if overrides else {}
|
|
overrides.setdefault("course_id", unicode(self.course.id))
|
|
return make_minimal_cs_thread(overrides)
|
|
|
|
def expected_response_comment(self, overrides=None):
|
|
"""
|
|
create expected response data
|
|
"""
|
|
response_data = {
|
|
"id": "test_comment",
|
|
"thread_id": self.thread_id,
|
|
"parent_id": None,
|
|
"author": self.author.username,
|
|
"author_label": None,
|
|
"created_at": "1970-01-01T00:00:00Z",
|
|
"updated_at": "1970-01-01T00:00:00Z",
|
|
"raw_body": "dummy",
|
|
"rendered_body": "<p>dummy</p>",
|
|
"endorsed": False,
|
|
"endorsed_by": None,
|
|
"endorsed_by_label": None,
|
|
"endorsed_at": None,
|
|
"abuse_flagged": False,
|
|
"voted": False,
|
|
"vote_count": 0,
|
|
"children": [],
|
|
"editable_fields": ["abuse_flagged", "voted"],
|
|
"child_count": 0,
|
|
}
|
|
response_data.update(overrides or {})
|
|
return response_data
|
|
|
|
def test_thread_id_missing(self):
|
|
response = self.client.get(self.url)
|
|
self.assert_response_correct(
|
|
response,
|
|
400,
|
|
{"field_errors": {"thread_id": {"developer_message": "This field is required."}}}
|
|
)
|
|
|
|
def test_404(self):
|
|
self.register_get_thread_error_response(self.thread_id, 404)
|
|
response = self.client.get(self.url, {"thread_id": self.thread_id})
|
|
self.assert_response_correct(
|
|
response,
|
|
404,
|
|
{"developer_message": "Thread not found."}
|
|
)
|
|
|
|
def test_basic(self):
|
|
self.register_get_user_response(self.user, upvoted_ids=["test_comment"])
|
|
source_comments = [
|
|
self.create_source_comment({"user_id": str(self.author.id), "username": self.author.username})
|
|
]
|
|
expected_comments = [self.expected_response_comment(overrides={
|
|
"voted": True,
|
|
"vote_count": 4,
|
|
"raw_body": "Test body",
|
|
"rendered_body": "<p>Test body</p>",
|
|
"created_at": "2015-05-11T00:00:00Z",
|
|
"updated_at": "2015-05-11T11:11:11Z",
|
|
})]
|
|
self.register_get_thread_response({
|
|
"id": self.thread_id,
|
|
"course_id": unicode(self.course.id),
|
|
"thread_type": "discussion",
|
|
"children": source_comments,
|
|
"resp_total": 100,
|
|
})
|
|
response = self.client.get(self.url, {"thread_id": self.thread_id})
|
|
next_link = "http://testserver/api/discussion/v1/comments/?page=2&thread_id={}".format(
|
|
self.thread_id
|
|
)
|
|
self.assert_response_correct(
|
|
response,
|
|
200,
|
|
make_paginated_api_response(
|
|
results=expected_comments, count=100, num_pages=10, next_link=next_link, previous_link=None
|
|
)
|
|
)
|
|
self.assert_query_params_equal(
|
|
httpretty.httpretty.latest_requests[-2],
|
|
{
|
|
"resp_skip": ["0"],
|
|
"resp_limit": ["10"],
|
|
"user_id": [str(self.user.id)],
|
|
"mark_as_read": ["False"],
|
|
"recursive": ["False"],
|
|
"with_responses": ["True"],
|
|
}
|
|
)
|
|
|
|
def test_pagination(self):
|
|
"""
|
|
Test that pagination parameters are correctly plumbed through to the
|
|
comments service and that a 404 is correctly returned if a page past the
|
|
end is requested
|
|
"""
|
|
self.register_get_user_response(self.user)
|
|
self.register_get_thread_response(make_minimal_cs_thread({
|
|
"id": self.thread_id,
|
|
"course_id": unicode(self.course.id),
|
|
"thread_type": "discussion",
|
|
"resp_total": 10,
|
|
}))
|
|
response = self.client.get(
|
|
self.url,
|
|
{"thread_id": self.thread_id, "page": "18", "page_size": "4"}
|
|
)
|
|
self.assert_response_correct(
|
|
response,
|
|
404,
|
|
{"developer_message": "Page not found (No results on this page)."}
|
|
)
|
|
self.assert_query_params_equal(
|
|
httpretty.httpretty.latest_requests[-2],
|
|
{
|
|
"resp_skip": ["68"],
|
|
"resp_limit": ["4"],
|
|
"user_id": [str(self.user.id)],
|
|
"mark_as_read": ["False"],
|
|
"recursive": ["False"],
|
|
"with_responses": ["True"],
|
|
}
|
|
)
|
|
|
|
@ddt.data(
|
|
(True, "endorsed_comment"),
|
|
("true", "endorsed_comment"),
|
|
("1", "endorsed_comment"),
|
|
(False, "non_endorsed_comment"),
|
|
("false", "non_endorsed_comment"),
|
|
("0", "non_endorsed_comment"),
|
|
)
|
|
@ddt.unpack
|
|
def test_question_content(self, endorsed, comment_id):
|
|
self.register_get_user_response(self.user)
|
|
thread = self.make_minimal_cs_thread({
|
|
"thread_type": "question",
|
|
"endorsed_responses": [make_minimal_cs_comment({
|
|
"id": "endorsed_comment",
|
|
"user_id": self.user.id,
|
|
"username": self.user.username,
|
|
})],
|
|
"non_endorsed_responses": [make_minimal_cs_comment({
|
|
"id": "non_endorsed_comment",
|
|
"user_id": self.user.id,
|
|
"username": self.user.username,
|
|
})],
|
|
"non_endorsed_resp_total": 1,
|
|
})
|
|
self.register_get_thread_response(thread)
|
|
response = self.client.get(self.url, {
|
|
"thread_id": thread["id"],
|
|
"endorsed": endorsed,
|
|
})
|
|
parsed_content = json.loads(response.content)
|
|
self.assertEqual(parsed_content["results"][0]["id"], comment_id)
|
|
|
|
def test_question_invalid_endorsed(self):
|
|
response = self.client.get(self.url, {
|
|
"thread_id": self.thread_id,
|
|
"endorsed": "invalid-boolean"
|
|
})
|
|
self.assert_response_correct(
|
|
response,
|
|
400,
|
|
{"field_errors": {
|
|
"endorsed": {"developer_message": "Invalid Boolean Value."}
|
|
}}
|
|
)
|
|
|
|
def test_question_missing_endorsed(self):
|
|
self.register_get_user_response(self.user)
|
|
thread = self.make_minimal_cs_thread({
|
|
"thread_type": "question",
|
|
"endorsed_responses": [make_minimal_cs_comment({"id": "endorsed_comment"})],
|
|
"non_endorsed_responses": [make_minimal_cs_comment({"id": "non_endorsed_comment"})],
|
|
"non_endorsed_resp_total": 1,
|
|
})
|
|
self.register_get_thread_response(thread)
|
|
response = self.client.get(self.url, {
|
|
"thread_id": thread["id"]
|
|
})
|
|
self.assert_response_correct(
|
|
response,
|
|
400,
|
|
{"field_errors": {
|
|
"endorsed": {"developer_message": "This field is required for question threads."}
|
|
}}
|
|
)
|
|
|
|
def test_child_comments_count(self):
|
|
self.register_get_user_response(self.user)
|
|
response_1 = make_minimal_cs_comment({
|
|
"id": "test_response_1",
|
|
"thread_id": self.thread_id,
|
|
"user_id": str(self.author.id),
|
|
"username": self.author.username,
|
|
"child_count": 2,
|
|
})
|
|
response_2 = make_minimal_cs_comment({
|
|
"id": "test_response_2",
|
|
"thread_id": self.thread_id,
|
|
"user_id": str(self.author.id),
|
|
"username": self.author.username,
|
|
"child_count": 3,
|
|
})
|
|
thread = self.make_minimal_cs_thread({
|
|
"id": self.thread_id,
|
|
"course_id": unicode(self.course.id),
|
|
"thread_type": "discussion",
|
|
"children": [response_1, response_2],
|
|
"resp_total": 2,
|
|
"comments_count": 8,
|
|
"unread_comments_count": 0,
|
|
|
|
})
|
|
self.register_get_thread_response(thread)
|
|
response = self.client.get(self.url, {"thread_id": self.thread_id})
|
|
expected_comments = [
|
|
self.expected_response_comment(overrides={"id": "test_response_1", "child_count": 2}),
|
|
self.expected_response_comment(overrides={"id": "test_response_2", "child_count": 3}),
|
|
]
|
|
self.assert_response_correct(
|
|
response,
|
|
200,
|
|
{
|
|
"results": expected_comments,
|
|
"pagination": {
|
|
"count": 2,
|
|
"next": None,
|
|
"num_pages": 1,
|
|
"previous": None,
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_profile_image_requested_field(self):
|
|
"""
|
|
Tests all comments retrieved have user profile image details if called in requested_fields
|
|
"""
|
|
source_comments = [self.create_source_comment()]
|
|
self.register_get_thread_response({
|
|
"id": self.thread_id,
|
|
"course_id": unicode(self.course.id),
|
|
"thread_type": "discussion",
|
|
"children": source_comments,
|
|
"resp_total": 100,
|
|
})
|
|
self.register_get_user_response(self.user, upvoted_ids=["test_comment"])
|
|
self.create_profile_image(self.user, get_profile_image_storage())
|
|
|
|
response = self.client.get(self.url, {"thread_id": self.thread_id, "requested_fields": "profile_image"})
|
|
self.assertEqual(response.status_code, 200)
|
|
response_comments = json.loads(response.content)['results']
|
|
for response_comment in response_comments:
|
|
expected_profile_data = self.get_expected_user_profile(response_comment['author'])
|
|
response_users = response_comment['users']
|
|
self.assertEqual(expected_profile_data, response_users[response_comment['author']])
|
|
|
|
def test_profile_image_requested_field_endorsed_comments(self):
|
|
"""
|
|
Tests all comments have user profile image details for both author and endorser
|
|
if called in requested_fields for endorsed threads
|
|
"""
|
|
endorser_user = UserFactory.create(password=self.password)
|
|
# Ensure that parental controls don't apply to this user
|
|
endorser_user.profile.year_of_birth = 1970
|
|
endorser_user.profile.save()
|
|
|
|
self.register_get_user_response(self.user)
|
|
thread = self.make_minimal_cs_thread({
|
|
"thread_type": "question",
|
|
"endorsed_responses": [make_minimal_cs_comment({
|
|
"id": "endorsed_comment",
|
|
"user_id": self.user.id,
|
|
"username": self.user.username,
|
|
"endorsed": True,
|
|
"endorsement": {"user_id": endorser_user.id, "time": "2016-05-10T08:51:28Z"},
|
|
})],
|
|
"non_endorsed_responses": [make_minimal_cs_comment({
|
|
"id": "non_endorsed_comment",
|
|
"user_id": self.user.id,
|
|
"username": self.user.username,
|
|
})],
|
|
"non_endorsed_resp_total": 1,
|
|
})
|
|
self.register_get_thread_response(thread)
|
|
self.create_profile_image(self.user, get_profile_image_storage())
|
|
self.create_profile_image(endorser_user, get_profile_image_storage())
|
|
|
|
response = self.client.get(self.url, {
|
|
"thread_id": thread["id"],
|
|
"endorsed": True,
|
|
"requested_fields": "profile_image",
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
response_comments = json.loads(response.content)['results']
|
|
for response_comment in response_comments:
|
|
expected_author_profile_data = self.get_expected_user_profile(response_comment['author'])
|
|
expected_endorser_profile_data = self.get_expected_user_profile(response_comment['endorsed_by'])
|
|
response_users = response_comment['users']
|
|
self.assertEqual(expected_author_profile_data, response_users[response_comment['author']])
|
|
self.assertEqual(expected_endorser_profile_data, response_users[response_comment['endorsed_by']])
|
|
|
|
def test_profile_image_request_for_null_endorsed_by(self):
|
|
"""
|
|
Tests if 'endorsed' is True but 'endorsed_by' is null, the api does not crash.
|
|
This is the case for some old/stale data in prod/stage environments.
|
|
"""
|
|
self.register_get_user_response(self.user)
|
|
thread = self.make_minimal_cs_thread({
|
|
"thread_type": "question",
|
|
"endorsed_responses": [make_minimal_cs_comment({
|
|
"id": "endorsed_comment",
|
|
"user_id": self.user.id,
|
|
"username": self.user.username,
|
|
"endorsed": True,
|
|
})],
|
|
"non_endorsed_resp_total": 0,
|
|
})
|
|
self.register_get_thread_response(thread)
|
|
self.create_profile_image(self.user, get_profile_image_storage())
|
|
|
|
response = self.client.get(self.url, {
|
|
"thread_id": thread["id"],
|
|
"endorsed": True,
|
|
"requested_fields": "profile_image",
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
response_comments = json.loads(response.content)['results']
|
|
for response_comment in response_comments:
|
|
expected_author_profile_data = self.get_expected_user_profile(response_comment['author'])
|
|
response_users = response_comment['users']
|
|
self.assertEqual(expected_author_profile_data, response_users[response_comment['author']])
|
|
self.assertNotIn(response_comment['endorsed_by'], response_users)
|
|
|
|
|
|
@httpretty.activate
|
|
@disable_signal(api, 'comment_deleted')
|
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
class CommentViewSetDeleteTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
|
|
"""Tests for ThreadViewSet delete"""
|
|
|
|
def setUp(self):
|
|
super(CommentViewSetDeleteTest, self).setUp()
|
|
self.url = reverse("comment-detail", kwargs={"comment_id": "test_comment"})
|
|
self.comment_id = "test_comment"
|
|
|
|
def test_basic(self):
|
|
self.register_get_user_response(self.user)
|
|
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": self.comment_id,
|
|
"course_id": cs_thread["course_id"],
|
|
"thread_id": cs_thread["id"],
|
|
"username": self.user.username,
|
|
"user_id": str(self.user.id),
|
|
})
|
|
self.register_get_comment_response(cs_comment)
|
|
self.register_delete_comment_response(self.comment_id)
|
|
response = self.client.delete(self.url)
|
|
self.assertEqual(response.status_code, 204)
|
|
self.assertEqual(response.content, "")
|
|
self.assertEqual(
|
|
urlparse(httpretty.last_request().path).path,
|
|
"/api/v1/comments/{}".format(self.comment_id)
|
|
)
|
|
self.assertEqual(httpretty.last_request().method, "DELETE")
|
|
|
|
def test_delete_nonexistent_comment(self):
|
|
self.register_get_comment_error_response(self.comment_id, 404)
|
|
response = self.client.delete(self.url)
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
|
|
@httpretty.activate
|
|
@disable_signal(api, 'comment_created')
|
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
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_thread()
|
|
self.register_comment()
|
|
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": "1970-01-01T00:00:00Z",
|
|
"updated_at": "1970-01-01T00:00:00Z",
|
|
"raw_body": "Test body",
|
|
"rendered_body": "<p>Test body</p>",
|
|
"endorsed": False,
|
|
"endorsed_by": None,
|
|
"endorsed_by_label": None,
|
|
"endorsed_at": None,
|
|
"abuse_flagged": False,
|
|
"voted": False,
|
|
"vote_count": 0,
|
|
"children": [],
|
|
"editable_fields": ["abuse_flagged", "raw_body", "voted"],
|
|
"child_count": 0,
|
|
}
|
|
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)
|
|
|
|
def test_closed_thread(self):
|
|
self.register_get_user_response(self.user)
|
|
self.register_thread({"closed": True})
|
|
self.register_comment()
|
|
request_data = {
|
|
"thread_id": "test_thread",
|
|
"raw_body": "Test body"
|
|
}
|
|
response = self.client.post(
|
|
self.url,
|
|
json.dumps(request_data),
|
|
content_type="application/json"
|
|
)
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
|
|
@ddt.ddt
|
|
@disable_signal(api, 'comment_edited')
|
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, PatchMediaTypeMixin):
|
|
"""Tests for CommentViewSet partial_update"""
|
|
def setUp(self):
|
|
self.unsupported_media_type = JSONParser.media_type
|
|
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"})
|
|
|
|
def expected_response_data(self, overrides=None):
|
|
"""
|
|
create expected response data from comment update endpoint
|
|
"""
|
|
response_data = {
|
|
"id": "test_comment",
|
|
"thread_id": "test_thread",
|
|
"parent_id": None,
|
|
"author": self.user.username,
|
|
"author_label": None,
|
|
"created_at": "1970-01-01T00:00:00Z",
|
|
"updated_at": "1970-01-01T00:00:00Z",
|
|
"raw_body": "Original body",
|
|
"rendered_body": "<p>Original body</p>",
|
|
"endorsed": False,
|
|
"endorsed_by": None,
|
|
"endorsed_by_label": None,
|
|
"endorsed_at": None,
|
|
"abuse_flagged": False,
|
|
"voted": False,
|
|
"vote_count": 0,
|
|
"children": [],
|
|
"editable_fields": [],
|
|
"child_count": 0,
|
|
}
|
|
response_data.update(overrides or {})
|
|
return response_data
|
|
|
|
def test_basic(self):
|
|
self.register_thread()
|
|
self.register_comment({"created_at": "Test Created Date", "updated_at": "Test Updated Date"})
|
|
request_data = {"raw_body": "Edited body"}
|
|
response = self.request_patch(request_data)
|
|
self.assertEqual(response.status_code, 200)
|
|
response_data = json.loads(response.content)
|
|
self.assertEqual(
|
|
response_data,
|
|
self.expected_response_data({
|
|
"raw_body": "Edited body",
|
|
"rendered_body": "<p>Edited body</p>",
|
|
"editable_fields": ["abuse_flagged", "raw_body", "voted"],
|
|
"created_at": "Test Created Date",
|
|
"updated_at": "Test Updated Date",
|
|
})
|
|
)
|
|
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):
|
|
self.register_thread()
|
|
self.register_comment()
|
|
request_data = {"raw_body": ""}
|
|
response = self.request_patch(request_data)
|
|
expected_response_data = {
|
|
"field_errors": {"raw_body": {"developer_message": "This field may not be blank."}}
|
|
}
|
|
self.assertEqual(response.status_code, 400)
|
|
response_data = json.loads(response.content)
|
|
self.assertEqual(response_data, expected_response_data)
|
|
|
|
@ddt.data(
|
|
("abuse_flagged", True),
|
|
("abuse_flagged", False),
|
|
)
|
|
@ddt.unpack
|
|
def test_closed_thread(self, field, value):
|
|
self.register_thread({"closed": True})
|
|
self.register_comment()
|
|
self.register_flag_response("comment", "test_comment")
|
|
request_data = {field: value}
|
|
response = self.request_patch(request_data)
|
|
self.assertEqual(response.status_code, 200)
|
|
response_data = json.loads(response.content)
|
|
self.assertEqual(
|
|
response_data,
|
|
self.expected_response_data({
|
|
"abuse_flagged": value,
|
|
"editable_fields": ["abuse_flagged"],
|
|
})
|
|
)
|
|
|
|
@ddt.data(
|
|
("raw_body", "Edited body"),
|
|
("voted", True),
|
|
("following", True),
|
|
)
|
|
@ddt.unpack
|
|
def test_closed_thread_error(self, field, value):
|
|
self.register_thread({"closed": True})
|
|
self.register_comment()
|
|
request_data = {field: value}
|
|
response = self.request_patch(request_data)
|
|
self.assertEqual(response.status_code, 400)
|
|
|
|
|
|
@httpretty.activate
|
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
class ThreadViewSetRetrieveTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, ProfileImageTestMixin):
|
|
"""Tests for ThreadViewSet Retrieve"""
|
|
def setUp(self):
|
|
super(ThreadViewSetRetrieveTest, self).setUp()
|
|
self.url = reverse("thread-detail", kwargs={"thread_id": "test_thread"})
|
|
self.thread_id = "test_thread"
|
|
|
|
def test_basic(self):
|
|
self.register_get_user_response(self.user)
|
|
cs_thread = make_minimal_cs_thread({
|
|
"id": self.thread_id,
|
|
"course_id": unicode(self.course.id),
|
|
"commentable_id": "test_topic",
|
|
"username": self.user.username,
|
|
"user_id": str(self.user.id),
|
|
"title": "Test Title",
|
|
"body": "Test body",
|
|
"created_at": "2015-05-29T00:00:00Z",
|
|
"updated_at": "2015-05-29T00:00:00Z"
|
|
})
|
|
expected_response_data = {
|
|
"author": self.user.username,
|
|
"author_label": None,
|
|
"created_at": "2015-05-29T00:00:00Z",
|
|
"updated_at": "2015-05-29T00:00:00Z",
|
|
"raw_body": "Test body",
|
|
"rendered_body": "<p>Test body</p>",
|
|
"abuse_flagged": False,
|
|
"voted": False,
|
|
"vote_count": 0,
|
|
"editable_fields": ["abuse_flagged", "following", "raw_body", "read", "title", "topic_id", "type", "voted"],
|
|
"course_id": unicode(self.course.id),
|
|
"topic_id": "test_topic",
|
|
"group_id": None,
|
|
"group_name": None,
|
|
"title": "Test Title",
|
|
"pinned": False,
|
|
"closed": False,
|
|
"following": False,
|
|
"comment_count": 1,
|
|
"unread_comment_count": 1,
|
|
"comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread",
|
|
"endorsed_comment_list_url": None,
|
|
"non_endorsed_comment_list_url": None,
|
|
"read": False,
|
|
"has_endorsed": False,
|
|
"id": "test_thread",
|
|
"type": "discussion",
|
|
"response_count": 0,
|
|
}
|
|
self.register_get_thread_response(cs_thread)
|
|
response = self.client.get(self.url)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(json.loads(response.content), expected_response_data)
|
|
self.assertEqual(httpretty.last_request().method, "GET")
|
|
|
|
def test_retrieve_nonexistent_thread(self):
|
|
self.register_get_thread_error_response(self.thread_id, 404)
|
|
response = self.client.get(self.url)
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
def test_profile_image_requested_field(self):
|
|
"""
|
|
Tests thread has user profile image details if called in requested_fields
|
|
"""
|
|
self.register_get_user_response(self.user)
|
|
cs_thread = make_minimal_cs_thread({
|
|
"id": self.thread_id,
|
|
"course_id": unicode(self.course.id),
|
|
"username": self.user.username,
|
|
"user_id": str(self.user.id),
|
|
})
|
|
self.register_get_thread_response(cs_thread)
|
|
self.create_profile_image(self.user, get_profile_image_storage())
|
|
response = self.client.get(self.url, {"requested_fields": "profile_image"})
|
|
self.assertEqual(response.status_code, 200)
|
|
expected_profile_data = self.get_expected_user_profile(self.user.username)
|
|
response_users = json.loads(response.content)['users']
|
|
self.assertEqual(expected_profile_data, response_users[self.user.username])
|
|
|
|
|
|
@httpretty.activate
|
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
class CommentViewSetRetrieveTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, ProfileImageTestMixin):
|
|
"""Tests for CommentViewSet Retrieve"""
|
|
def setUp(self):
|
|
super(CommentViewSetRetrieveTest, self).setUp()
|
|
self.url = reverse("comment-detail", kwargs={"comment_id": "test_comment"})
|
|
self.thread_id = "test_thread"
|
|
self.comment_id = "test_comment"
|
|
|
|
def make_comment_data(self, comment_id, parent_id=None, children=[]): # pylint: disable=W0102
|
|
"""
|
|
Returns comment dict object as returned by comments service
|
|
"""
|
|
return make_minimal_cs_comment({
|
|
"id": comment_id,
|
|
"parent_id": parent_id,
|
|
"course_id": unicode(self.course.id),
|
|
"thread_id": self.thread_id,
|
|
"thread_type": "discussion",
|
|
"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",
|
|
"children": children,
|
|
})
|
|
|
|
def test_basic(self):
|
|
self.register_get_user_response(self.user)
|
|
cs_comment_child = self.make_comment_data("test_child_comment", self.comment_id, children=[])
|
|
cs_comment = self.make_comment_data(self.comment_id, None, [cs_comment_child])
|
|
cs_thread = make_minimal_cs_thread({
|
|
"id": self.thread_id,
|
|
"course_id": unicode(self.course.id),
|
|
"children": [cs_comment],
|
|
})
|
|
self.register_get_thread_response(cs_thread)
|
|
self.register_get_comment_response(cs_comment)
|
|
|
|
expected_response_data = {
|
|
"id": "test_child_comment",
|
|
"parent_id": self.comment_id,
|
|
"thread_id": self.thread_id,
|
|
"author": self.user.username,
|
|
"author_label": None,
|
|
"raw_body": "Original body",
|
|
"rendered_body": "<p>Original body</p>",
|
|
"created_at": "2015-06-03T00:00:00Z",
|
|
"updated_at": "2015-06-03T00:00:00Z",
|
|
"children": [],
|
|
"endorsed_at": None,
|
|
"endorsed": False,
|
|
"endorsed_by": None,
|
|
"endorsed_by_label": None,
|
|
"voted": False,
|
|
"vote_count": 0,
|
|
"abuse_flagged": False,
|
|
"editable_fields": ["abuse_flagged", "raw_body", "voted"],
|
|
"child_count": 0,
|
|
}
|
|
|
|
response = self.client.get(self.url)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(json.loads(response.content)['results'][0], expected_response_data)
|
|
|
|
def test_retrieve_nonexistent_comment(self):
|
|
self.register_get_comment_error_response(self.comment_id, 404)
|
|
response = self.client.get(self.url)
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
def test_pagination(self):
|
|
"""
|
|
Test that pagination parameters are correctly plumbed through to the
|
|
comments service and that a 404 is correctly returned if a page past the
|
|
end is requested
|
|
"""
|
|
self.register_get_user_response(self.user)
|
|
cs_comment_child = self.make_comment_data("test_child_comment", self.comment_id, children=[])
|
|
cs_comment = self.make_comment_data(self.comment_id, None, [cs_comment_child])
|
|
cs_thread = make_minimal_cs_thread({
|
|
"id": self.thread_id,
|
|
"course_id": unicode(self.course.id),
|
|
"children": [cs_comment],
|
|
})
|
|
self.register_get_thread_response(cs_thread)
|
|
self.register_get_comment_response(cs_comment)
|
|
response = self.client.get(
|
|
self.url,
|
|
{"comment_id": self.comment_id, "page": "18", "page_size": "4"}
|
|
)
|
|
self.assert_response_correct(
|
|
response,
|
|
404,
|
|
{"developer_message": "Page not found (No results on this page)."}
|
|
)
|
|
|
|
def test_profile_image_requested_field(self):
|
|
"""
|
|
Tests all comments retrieved have user profile image details if called in requested_fields
|
|
"""
|
|
self.register_get_user_response(self.user)
|
|
cs_comment_child = self.make_comment_data('test_child_comment', self.comment_id, children=[])
|
|
cs_comment = self.make_comment_data(self.comment_id, None, [cs_comment_child])
|
|
cs_thread = make_minimal_cs_thread({
|
|
'id': self.thread_id,
|
|
'course_id': unicode(self.course.id),
|
|
'children': [cs_comment],
|
|
})
|
|
self.register_get_thread_response(cs_thread)
|
|
self.register_get_comment_response(cs_comment)
|
|
self.create_profile_image(self.user, get_profile_image_storage())
|
|
|
|
response = self.client.get(self.url, {'requested_fields': 'profile_image'})
|
|
self.assertEqual(response.status_code, 200)
|
|
response_comments = json.loads(response.content)['results']
|
|
|
|
for response_comment in response_comments:
|
|
expected_profile_data = self.get_expected_user_profile(response_comment['author'])
|
|
response_users = response_comment['users']
|
|
self.assertEqual(expected_profile_data, response_users[response_comment['author']])
|