Commit 5d8d1619 by christopher lee

Added unanswered/unread query params for thread in discussion api

Discusion API now takes a view parameter when getting a thread list.
This view parameter only takes "unread" or "unanswered" as possible
values.
parent 81e677b7
......@@ -231,7 +231,15 @@ def get_course_topics(request, course_key):
}
def get_thread_list(request, course_key, page, page_size, topic_id_list=None, text_search=None, following=False):
def get_thread_list(
request,
course_key,
page,
page_size,
topic_id_list=None,
text_search=None,
following=False,
view=None):
"""
Return the list of all discussion threads pertaining to the given course
......@@ -244,6 +252,7 @@ def get_thread_list(request, course_key, page, page_size, topic_id_list=None, te
topic_id_list: The list of topic_ids to get the discussion threads for
text_search A text search query string to match
following: If true, retrieve only threads the requester is following
view: filters for either "unread" or "unanswered" threads
Note that topic_id_list, text_search, and following are mutually exclusive.
......@@ -254,6 +263,7 @@ def get_thread_list(request, course_key, page, page_size, topic_id_list=None, te
Raises:
ValidationError: if an invalid value is passed for a field
ValueError: if more than one of the mutually exclusive parameters is
provided
Http404: if the requesting user does not have access to the requested course
......@@ -266,6 +276,7 @@ def get_thread_list(request, course_key, page, page_size, topic_id_list=None, te
course = _get_course_or_404(course_key, request.user)
context = get_context(course, request)
query_params = {
"user_id": unicode(request.user.id),
"group_id": (
None if context["is_requester_privileged"] else
get_cohort_id(request.user, course.id)
......@@ -276,7 +287,17 @@ def get_thread_list(request, course_key, page, page_size, topic_id_list=None, te
"per_page": page_size,
"text": text_search,
}
text_search_rewrite = None
if view:
if view in ["unread", "unanswered"]:
query_params[view] = "true"
else:
ValidationError({
"view": ["Invalid value. '{}' must be 'unread' or 'unanswered'".format(view)]
})
if following:
threads, result_page, num_pages = context["cc_requester"].subscribed_threads(query_params)
else:
......
......@@ -5,6 +5,7 @@ from django.core.exceptions import ValidationError
from django.forms import (
BooleanField,
CharField,
ChoiceField,
Field,
Form,
IntegerField,
......@@ -51,6 +52,10 @@ class ThreadListGetForm(_PaginationForm):
topic_id = TopicIdField(required=False)
text_search = CharField(required=False)
following = NullBooleanField(required=False)
view = ChoiceField(
choices=[(choice, choice) for choice in ["unread", "unanswered"]],
required=False
)
def clean_course_id(self):
"""Validate course_id"""
......
......@@ -185,6 +185,8 @@ class ThreadSerializer(_ContentSerializer):
comment_list_url = serializers.SerializerMethodField("get_comment_list_url")
endorsed_comment_list_url = serializers.SerializerMethodField("get_endorsed_comment_list_url")
non_endorsed_comment_list_url = serializers.SerializerMethodField("get_non_endorsed_comment_list_url")
read = serializers.BooleanField(read_only=True)
has_endorsed = serializers.BooleanField(read_only=True, source="endorsed")
non_updatable_fields = NON_UPDATABLE_THREAD_FIELDS
......
......@@ -521,6 +521,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
self.get_thread_list([], topic_id_list=["topic_x", "topic_meow"])
self.assertEqual(urlparse(httpretty.last_request().path).path, "/api/v1/threads")
self.assert_last_query_params({
"user_id": [unicode(self.user.id)],
"course_id": [unicode(self.course.id)],
"sort_key": ["date"],
"sort_order": ["desc"],
......@@ -533,6 +534,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
def test_basic_query_params(self):
self.get_thread_list([], page=6, page_size=14)
self.assert_last_query_params({
"user_id": [unicode(self.user.id)],
"course_id": [unicode(self.course.id)],
"sort_key": ["date"],
"sort_order": ["desc"],
......@@ -564,6 +566,8 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
"votes": {"up_count": 4},
"comments_count": 5,
"unread_comments_count": 3,
"endorsed": True,
"read": True,
},
{
"type": "thread",
......@@ -586,6 +590,8 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
"votes": {"up_count": 9},
"comments_count": 18,
"unread_comments_count": 0,
"endorsed": False,
"read": False,
},
]
expected_threads = [
......@@ -615,6 +621,8 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
"endorsed_comment_list_url": None,
"non_endorsed_comment_list_url": None,
"editable_fields": ["abuse_flagged", "following", "voted"],
"has_endorsed": True,
"read": True,
},
{
"id": "test_thread_id_1",
......@@ -646,6 +654,8 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
"http://testserver/api/discussion/v1/comments/?thread_id=test_thread_id_1&endorsed=False"
),
"editable_fields": ["abuse_flagged", "following", "voted"],
"has_endorsed": False,
"read": False,
},
]
self.assertEqual(
......@@ -735,6 +745,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
}
)
self.assert_last_query_params({
"user_id": [unicode(self.user.id)],
"course_id": [unicode(self.course.id)],
"sort_key": ["date"],
"sort_order": ["desc"],
......@@ -762,6 +773,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
"/api/v1/users/{}/subscribed_threads".format(self.user.id)
)
self.assert_last_query_params({
"user_id": [unicode(self.user.id)],
"course_id": [unicode(self.course.id)],
"sort_key": ["date"],
"sort_order": ["desc"],
......@@ -769,6 +781,35 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest
"per_page": ["11"],
})
@ddt.data("unanswered", "unread")
def test_view_query(self, query):
self.register_get_threads_response([], page=1, num_pages=1)
result = get_thread_list(
self.request,
self.course.id,
page=1,
page_size=11,
view=query,
)
self.assertEqual(
result,
{"results": [], "next": None, "previous": None, "text_search_rewrite": None}
)
self.assertEqual(
urlparse(httpretty.last_request().path).path,
"/api/v1/threads"
)
self.assert_last_query_params({
"user_id": [unicode(self.user.id)],
"course_id": [unicode(self.course.id)],
"sort_key": ["date"],
"sort_order": ["desc"],
"page": ["1"],
"per_page": ["11"],
"recursive": ["False"],
query: ["true"],
})
@ddt.ddt
class GetCommentListTest(CommentsServiceMockMixin, ModuleStoreTestCase):
......@@ -1240,6 +1281,8 @@ class CreateThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestC
"endorsed_comment_list_url": None,
"non_endorsed_comment_list_url": None,
"editable_fields": ["abuse_flagged", "following", "raw_body", "title", "topic_id", "type", "voted"],
'read': False,
'has_endorsed': False
}
self.assertEqual(actual, expected)
self.assertEqual(
......@@ -1745,6 +1788,8 @@ class UpdateThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestC
"endorsed_comment_list_url": None,
"non_endorsed_comment_list_url": None,
"editable_fields": ["abuse_flagged", "following", "raw_body", "title", "topic_id", "type", "voted"],
'read': False,
'has_endorsed': False,
}
self.assertEqual(actual, expected)
self.assertEqual(
......
......@@ -95,6 +95,7 @@ class ThreadListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase):
"topic_id": [],
"text_search": "",
"following": None,
"view": ""
}
)
......@@ -142,6 +143,10 @@ class ThreadListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase):
"The following query parameters are mutually exclusive: topic_id, text_search, following"
)
def test_invalid_view_choice(self):
self.form_data["view"] = "not_a_valid_choice"
self.assert_error("view", "Select a valid choice. not_a_valid_choice is not one of the available choices.")
class CommentListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase):
"""Tests for CommentListGetForm"""
......
......@@ -138,6 +138,8 @@ class ThreadSerializerSerializationTest(SerializerTestMixin, ModuleStoreTestCase
"course_id": unicode(self.course.id),
"user_id": str(self.author.id),
"username": self.author.username,
"read": True,
"endorsed": True
}
merged_overrides.update(overrides)
return make_minimal_cs_thread(merged_overrides)
......@@ -171,6 +173,8 @@ class ThreadSerializerSerializationTest(SerializerTestMixin, ModuleStoreTestCase
"votes": {"up_count": 4},
"comments_count": 5,
"unread_comments_count": 3,
"read": False,
"endorsed": False
}
expected = {
"id": "test_thread",
......@@ -198,6 +202,8 @@ class ThreadSerializerSerializationTest(SerializerTestMixin, ModuleStoreTestCase
"endorsed_comment_list_url": None,
"non_endorsed_comment_list_url": None,
"editable_fields": ["abuse_flagged", "following", "voted"],
"read": False,
"has_endorsed": False
}
self.assertEqual(self.serialize(thread), expected)
......@@ -424,6 +430,8 @@ class ThreadSerializerDeserializationTest(CommentsServiceMockMixin, UrlResetMixi
"title": "Original Title",
"body": "Original body",
"user_id": str(self.user.id),
"read": "False",
"endorsed": "False"
}))
def save_and_reserialize(self, data, instance=None):
......
......@@ -5,6 +5,7 @@ from datetime import datetime
import json
from urlparse import urlparse
import ddt
import httpretty
import mock
from pytz import UTC
......@@ -134,6 +135,7 @@ class CourseTopicsViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
)
@ddt.ddt
@httpretty.activate
class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"""Tests for ThreadViewSet list"""
......@@ -181,6 +183,8 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"votes": {"up_count": 4},
"comments_count": 5,
"unread_comments_count": 3,
"read": False,
"endorsed": False
}]
expected_threads = [{
"id": "test_thread",
......@@ -208,6 +212,8 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"endorsed_comment_list_url": None,
"non_endorsed_comment_list_url": None,
"editable_fields": ["abuse_flagged", "following", "voted"],
"read": False,
"has_endorsed": False
}]
self.register_get_threads_response(source_threads, page=1, num_pages=2)
response = self.client.get(self.url, {"course_id": unicode(self.course.id)})
......@@ -222,6 +228,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
}
)
self.assert_last_query_params({
"user_id": [unicode(self.user.id)],
"course_id": [unicode(self.course.id)],
"sort_key": ["date"],
"sort_order": ["desc"],
......@@ -230,6 +237,29 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"recursive": ["False"],
})
@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": ["date"],
"sort_order": ["desc"],
"recursive": ["False"],
"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)
......@@ -243,6 +273,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
{"developer_message": "Not found."}
)
self.assert_last_query_params({
"user_id": [unicode(self.user.id)],
"course_id": [unicode(self.course.id)],
"sort_key": ["date"],
"sort_order": ["desc"],
......@@ -264,6 +295,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
{"results": [], "next": None, "previous": None, "text_search_rewrite": None}
)
self.assert_last_query_params({
"user_id": [unicode(self.user.id)],
"course_id": [unicode(self.course.id)],
"sort_key": ["date"],
"sort_order": ["desc"],
......@@ -344,6 +376,8 @@ class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"endorsed_comment_list_url": None,
"non_endorsed_comment_list_url": None,
"editable_fields": ["abuse_flagged", "following", "raw_body", "title", "topic_id", "type", "voted"],
"read": False,
"has_endorsed": False
}
response = self.client.post(
self.url,
......@@ -435,6 +469,8 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest
"endorsed_comment_list_url": None,
"non_endorsed_comment_list_url": None,
"editable_fields": ["abuse_flagged", "following", "raw_body", "title", "topic_id", "type", "voted"],
"read": False,
"has_endorsed": False
}
response = self.client.patch( # pylint: disable=no-member
self.url,
......
......@@ -330,6 +330,8 @@ def make_minimal_cs_thread(overrides=None):
"unread_comments_count": 0,
"children": [],
"resp_total": 0,
"read": False,
"endorsed": False,
}
ret.update(overrides or {})
return ret
......
......@@ -117,7 +117,7 @@ class ThreadViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
"topic_id": "quux",
"type": "discussion",
"title": "Title text",
"body": "Body text"
"raw_body": "Body text"
}
PATCH /api/discussion/v1/threads/thread_id
......@@ -144,6 +144,10 @@ class ThreadViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
* following: If true, retrieve only threads the requesting user is
following
* view: "unread" for threads the requesting user has not read, or
"unanswered" for question threads with no marked answer. Only one
can be selected.
The topic_id, text_search, and following parameters are mutually
exclusive (i.e. only one may be specified in a request)
......@@ -212,6 +216,10 @@ class ThreadViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
* editable_fields: The fields that the requesting user is allowed to
modify with a PATCH request
* read: Boolean indicating whether the user has read this thread
* has_endorsed: Boolean indicating whether this thread has been answered
**DELETE response values:
No content is returned for a DELETE request
......@@ -236,6 +244,7 @@ class ThreadViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
form.cleaned_data["topic_id"],
form.cleaned_data["text_search"],
form.cleaned_data["following"],
form.cleaned_data["view"],
)
)
......
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