Commit 384fa77e by Christopher Lee

Merge pull request #8585 from edx/clee/discussion-api-order-and-sort-threads

Thread sort key and direction for discussion api
parents 1df53ac3 ed3bb27d
...@@ -239,7 +239,10 @@ def get_thread_list( ...@@ -239,7 +239,10 @@ def get_thread_list(
topic_id_list=None, topic_id_list=None,
text_search=None, text_search=None,
following=False, following=False,
view=None): view=None,
order_by="last_activity_at",
order_direction="desc",
):
""" """
Return the list of all discussion threads pertaining to the given course Return the list of all discussion threads pertaining to the given course
...@@ -253,6 +256,11 @@ def get_thread_list( ...@@ -253,6 +256,11 @@ def get_thread_list(
text_search A text search query string to match text_search A text search query string to match
following: If true, retrieve only threads the requester is following following: If true, retrieve only threads the requester is following
view: filters for either "unread" or "unanswered" threads view: filters for either "unread" or "unanswered" threads
order_by: The key in which to sort the threads by. The only values are
"last_activity_at", "comment_count", and "vote_count". The default is
"last_activity_at".
order_direction: The direction in which to sort the threads by. The only
values are "asc" or "desc". The default is "desc".
Note that topic_id_list, text_search, and following are mutually exclusive. Note that topic_id_list, text_search, and following are mutually exclusive.
...@@ -263,7 +271,7 @@ def get_thread_list( ...@@ -263,7 +271,7 @@ def get_thread_list(
Raises: Raises:
ValidationError: if an invalid value is passed for a field ValidationError: if an invalid value is passed for a field.
ValueError: if more than one of the mutually exclusive parameters is ValueError: if more than one of the mutually exclusive parameters is
provided provided
Http404: if the requesting user does not have access to the requested course Http404: if the requesting user does not have access to the requested course
...@@ -273,19 +281,31 @@ def get_thread_list( ...@@ -273,19 +281,31 @@ def get_thread_list(
if exclusive_param_count > 1: # pragma: no cover if exclusive_param_count > 1: # pragma: no cover
raise ValueError("More than one mutually exclusive param passed to get_thread_list") raise ValueError("More than one mutually exclusive param passed to get_thread_list")
cc_map = {"last_activity_at": "date", "comment_count": "comments", "vote_count": "votes"}
if order_by not in cc_map:
raise ValidationError({
"order_by":
["Invalid value. '{}' must be 'last_activity_at', 'comment_count', or 'vote_count'".format(order_by)]
})
if order_direction not in ["asc", "desc"]:
raise ValidationError({
"order_direction": ["Invalid value. '{}' must be 'asc' or 'desc'".format(order_direction)]
})
course = _get_course_or_404(course_key, request.user) course = _get_course_or_404(course_key, request.user)
context = get_context(course, request) context = get_context(course, request)
query_params = { query_params = {
"user_id": unicode(request.user.id), "user_id": unicode(request.user.id),
"group_id": ( "group_id": (
None if context["is_requester_privileged"] else None if context["is_requester_privileged"] else
get_cohort_id(request.user, course.id) get_cohort_id(request.user, course.id)
), ),
"sort_key": "date",
"sort_order": "desc",
"page": page, "page": page,
"per_page": page_size, "per_page": page_size,
"text": text_search, "text": text_search,
"sort_key": cc_map.get(order_by),
"sort_order": order_direction,
} }
text_search_rewrite = None text_search_rewrite = None
......
...@@ -54,9 +54,25 @@ class ThreadListGetForm(_PaginationForm): ...@@ -54,9 +54,25 @@ class ThreadListGetForm(_PaginationForm):
following = NullBooleanField(required=False) following = NullBooleanField(required=False)
view = ChoiceField( view = ChoiceField(
choices=[(choice, choice) for choice in ["unread", "unanswered"]], choices=[(choice, choice) for choice in ["unread", "unanswered"]],
required=False,
)
order_by = ChoiceField(
choices=[(choice, choice) for choice in ["last_activity_at", "comment_count", "vote_count"]],
required=False
)
order_direction = ChoiceField(
choices=[(choice, choice) for choice in ["asc", "desc"]],
required=False required=False
) )
def clean_order_by(self):
"""Return a default choice"""
return self.cleaned_data.get("order_by") or "last_activity_at"
def clean_order_direction(self):
"""Return a default choice"""
return self.cleaned_data.get("order_direction") or "desc"
def clean_course_id(self): def clean_course_id(self):
"""Validate course_id""" """Validate course_id"""
value = self.cleaned_data["course_id"] value = self.cleaned_data["course_id"]
......
...@@ -836,6 +836,74 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, SharedModuleSto ...@@ -836,6 +836,74 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, SharedModuleSto
query: ["true"], query: ["true"],
}) })
@ddt.data(
("last_activity_at", "date"),
("comment_count", "comments"),
("vote_count", "votes")
)
@ddt.unpack
def test_order_by_query(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
"""
self.register_get_threads_response([], page=1, num_pages=1)
result = get_thread_list(
self.request,
self.course.id,
page=1,
page_size=11,
order_by=http_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": [cc_query],
"sort_order": ["desc"],
"page": ["1"],
"per_page": ["11"],
"recursive": ["False"],
})
@ddt.data("asc", "desc")
def test_order_direction_query(self, http_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,
order_direction=http_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": [http_query],
"page": ["1"],
"per_page": ["11"],
"recursive": ["False"],
})
@ddt.ddt @ddt.ddt
class GetCommentListTest(CommentsServiceMockMixin, SharedModuleStoreTestCase): class GetCommentListTest(CommentsServiceMockMixin, SharedModuleStoreTestCase):
......
...@@ -95,7 +95,9 @@ class ThreadListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase): ...@@ -95,7 +95,9 @@ class ThreadListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase):
"topic_id": [], "topic_id": [],
"text_search": "", "text_search": "",
"following": None, "following": None,
"view": "" "view": "",
"order_by": "last_activity_at",
"order_direction": "desc",
} }
) )
...@@ -147,6 +149,20 @@ class ThreadListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase): ...@@ -147,6 +149,20 @@ class ThreadListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase):
self.form_data["view"] = "not_a_valid_choice" 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.") self.assert_error("view", "Select a valid choice. not_a_valid_choice is not one of the available choices.")
def test_invalid_sort_by_choice(self):
self.form_data["order_by"] = "not_a_valid_choice"
self.assert_error(
"order_by",
"Select a valid choice. not_a_valid_choice is not one of the available choices."
)
def test_invalid_sort_direction_choice(self):
self.form_data["order_direction"] = "not_a_valid_choice"
self.assert_error(
"order_direction",
"Select a valid choice. not_a_valid_choice is not one of the available choices."
)
class CommentListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase): class CommentListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase):
"""Tests for CommentListGetForm""" """Tests for CommentListGetForm"""
......
...@@ -327,6 +327,62 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): ...@@ -327,6 +327,62 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"/api/v1/users/{}/subscribed_threads".format(self.user.id) "/api/v1/users/{}/subscribed_threads".format(self.user.id)
) )
@ddt.data(
("last_activity_at", "date"),
("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)],
"sort_order": ["desc"],
"recursive": ["False"],
"page": ["1"],
"per_page": ["10"],
"sort_key": [cc_query],
})
@ddt.data("asc", "desc")
def test_order_direction(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),
"order_direction": query,
}
)
self.assert_last_query_params({
"user_id": [unicode(self.user.id)],
"course_id": [unicode(self.course.id)],
"sort_key": ["date"],
"recursive": ["False"],
"page": ["1"],
"per_page": ["10"],
"sort_order": [query],
})
@httpretty.activate @httpretty.activate
class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
......
...@@ -141,6 +141,13 @@ class ThreadViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet): ...@@ -141,6 +141,13 @@ class ThreadViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
(including the bodies of comments in the thread) matches the search (including the bodies of comments in the thread) matches the search
string will be returned. string will be returned.
* order_by: Must be "last_activity_at", "comment_count", or
"vote_count". The key to sort the threads by. The default is
"last_activity_at".
* order_direction: Must be "asc" or "desc". The direction in which to
sort the threads by. The default is "desc".
* following: If true, retrieve only threads the requesting user is * following: If true, retrieve only threads the requesting user is
following following
...@@ -245,6 +252,8 @@ class ThreadViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet): ...@@ -245,6 +252,8 @@ class ThreadViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet):
form.cleaned_data["text_search"], form.cleaned_data["text_search"],
form.cleaned_data["following"], form.cleaned_data["following"],
form.cleaned_data["view"], form.cleaned_data["view"],
form.cleaned_data["order_by"],
form.cleaned_data["order_direction"],
) )
) )
......
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