Пример #1
0
def _get_thread_and_context(request, thread_id, retrieve_kwargs=None):
    """
    Retrieve the given thread and build a serializer context for it, returning
    both. This function also enforces access control for the thread (checking
    both the user's access to the course and to the thread's cohort if
    applicable). Raises ThreadNotFoundError if the thread does not exist or the
    user cannot access it.
    """
    retrieve_kwargs = retrieve_kwargs or {}
    try:
        if "with_responses" not in retrieve_kwargs:
            retrieve_kwargs["with_responses"] = False
        if "mark_as_read" not in retrieve_kwargs:
            retrieve_kwargs["mark_as_read"] = False
        cc_thread = Thread(id=thread_id).retrieve(**retrieve_kwargs)
        course_key = CourseKey.from_string(cc_thread["course_id"])
        course = _get_course(course_key, request.user)
        context = get_context(course, request, cc_thread)
        course_discussion_settings = get_course_discussion_settings(course_key)
        if (
                not context["is_requester_privileged"] and
                cc_thread["group_id"] and
                is_commentable_divided(course.id, cc_thread["commentable_id"], course_discussion_settings)
        ):
            requester_group_id = get_group_id_for_user(request.user, course_discussion_settings)
            if requester_group_id is not None and cc_thread["group_id"] != requester_group_id:
                raise ThreadNotFoundError("Thread not found.")
        return cc_thread, context
    except CommentClientRequestError:
        # params are validated at a higher level, so the only possible request
        # error is if the thread doesn't exist
        raise ThreadNotFoundError("Thread not found.")
Пример #2
0
 def test_create_parent_id_too_deep(self, max_depth):
     with mock.patch("lms.djangoapps.discussion.django_comment_client.utils.MAX_COMMENT_DEPTH", max_depth):
         data = self.minimal_data.copy()
         context = get_context(self.course, self.request, make_minimal_cs_thread())
         if max_depth is None or max_depth >= 0:
             if max_depth != 0:
                 self.register_get_comment_response({
                     "id": "not_too_deep",
                     "thread_id": "test_thread",
                     "depth": max_depth - 1 if max_depth else 100
                 })
                 data["parent_id"] = "not_too_deep"
             else:
                 data["parent_id"] = None
             serializer = CommentSerializer(data=data, context=context)
             assert serializer.is_valid(), serializer.errors
         if max_depth is not None:
             if max_depth >= 0:
                 self.register_get_comment_response({
                     "id": "too_deep",
                     "thread_id": "test_thread",
                     "depth": max_depth
                 })
                 data["parent_id"] = "too_deep"
             else:
                 data["parent_id"] = None
             serializer = CommentSerializer(data=data, context=context)
             assert not serializer.is_valid()
             assert serializer.errors == {'non_field_errors': ['Comment level is too deep.']}
Пример #3
0
 def serialize(self, comment, thread_data=None):
     """
     Create a serializer with an appropriate context and use it to serialize
     the given comment, returning the result.
     """
     context = get_context(self.course, self.request, make_minimal_cs_thread(thread_data))
     return CommentSerializer(comment, context=context).data
Пример #4
0
def _get_thread_and_context(request, thread_id, retrieve_kwargs=None):
    """
    Retrieve the given thread and build a serializer context for it, returning
    both. This function also enforces access control for the thread (checking
    both the user's access to the course and to the thread's cohort if
    applicable). Raises ThreadNotFoundError if the thread does not exist or the
    user cannot access it.
    """
    retrieve_kwargs = retrieve_kwargs or {}
    try:
        if "with_responses" not in retrieve_kwargs:
            retrieve_kwargs["with_responses"] = False
        if "mark_as_read" not in retrieve_kwargs:
            retrieve_kwargs["mark_as_read"] = False
        cc_thread = Thread(id=thread_id).retrieve(**retrieve_kwargs)
        course_key = CourseKey.from_string(cc_thread["course_id"])
        course = _get_course(course_key, request.user)
        context = get_context(course, request, cc_thread)
        course_discussion_settings = get_course_discussion_settings(course_key)
        if (not context["is_requester_privileged"]
                and cc_thread["group_id"] and is_commentable_divided(
                    course.id, cc_thread["commentable_id"],
                    course_discussion_settings)):
            requester_group_id = get_group_id_for_user(
                request.user, course_discussion_settings)
            if requester_group_id is not None and cc_thread[
                    "group_id"] != requester_group_id:
                raise ThreadNotFoundError("Thread not found.")
        return cc_thread, context
    except CommentClientRequestError:
        # params are validated at a higher level, so the only possible request
        # error is if the thread doesn't exist
        raise ThreadNotFoundError("Thread not found.")
Пример #5
0
 def serialize(self, comment, thread_data=None):
     """
     Create a serializer with an appropriate context and use it to serialize
     the given comment, returning the result.
     """
     context = get_context(self.course, self.request, make_minimal_cs_thread(thread_data))
     return CommentSerializer(comment, context=context).data
Пример #6
0
 def test_create_parent_id_too_deep(self, max_depth):
     with mock.patch("lms.djangoapps.discussion.django_comment_client.utils.MAX_COMMENT_DEPTH", max_depth):
         data = self.minimal_data.copy()
         context = get_context(self.course, self.request, make_minimal_cs_thread())
         if max_depth is None or max_depth >= 0:
             if max_depth != 0:
                 self.register_get_comment_response({
                     "id": "not_too_deep",
                     "thread_id": "test_thread",
                     "depth": max_depth - 1 if max_depth else 100
                 })
                 data["parent_id"] = "not_too_deep"
             else:
                 data["parent_id"] = None
             serializer = CommentSerializer(data=data, context=context)
             self.assertTrue(serializer.is_valid(), serializer.errors)
         if max_depth is not None:
             if max_depth >= 0:
                 self.register_get_comment_response({
                     "id": "too_deep",
                     "thread_id": "test_thread",
                     "depth": max_depth
                 })
                 data["parent_id"] = "too_deep"
             else:
                 data["parent_id"] = None
             serializer = CommentSerializer(data=data, context=context)
             self.assertFalse(serializer.is_valid())
             self.assertEqual(serializer.errors, {"non_field_errors": ["Comment level is too deep."]})
Пример #7
0
 def serialize(self, thread):
     """
     Create a serializer with an appropriate context and use it to serialize
     the given thread, returning the result.
     """
     return ThreadSerializer(thread,
                             context=get_context(self.course,
                                                 self.request)).data
Пример #8
0
 def test_create_empty_string(self, value):
     data = self.minimal_data.copy()
     data.update({field: value for field in ["topic_id", "title", "raw_body"]})
     serializer = ThreadSerializer(data=data, context=get_context(self.course, self.request))
     assert not serializer.is_valid()
     assert serializer.errors == {
         field: ['This field may not be blank.'] for field in ['topic_id', 'title', 'raw_body']
     }
Пример #9
0
 def test_update_empty_raw_body(self, value):
     serializer = CommentSerializer(
         self.existing_comment,
         data={"raw_body": value},
         partial=True,
         context=get_context(self.course, self.request)
     )
     assert not serializer.is_valid()
     assert serializer.errors == {'raw_body': ['This field may not be blank.']}
Пример #10
0
 def test_update_course_id(self):
     serializer = ThreadSerializer(
         self.existing_thread,
         data={"course_id": "some/other/course"},
         partial=True,
         context=get_context(self.course, self.request)
     )
     assert not serializer.is_valid()
     assert serializer.errors == {'course_id': ['This field is not allowed in an update.']}
Пример #11
0
 def test_create_empty_string(self, value):
     data = self.minimal_data.copy()
     data.update({field: value for field in ["topic_id", "title", "raw_body"]})
     serializer = ThreadSerializer(data=data, context=get_context(self.course, self.request))
     self.assertFalse(serializer.is_valid())
     self.assertEqual(
         serializer.errors,
         {field: ["This field may not be blank."] for field in ["topic_id", "title", "raw_body"]}
     )
Пример #12
0
 def test_create_empty_string(self, value):
     data = self.minimal_data.copy()
     data.update({field: value for field in ["topic_id", "title", "raw_body"]})
     serializer = ThreadSerializer(data=data, context=get_context(self.course, self.request))
     self.assertFalse(serializer.is_valid())
     self.assertEqual(
         serializer.errors,
         {field: ["This field may not be blank."] for field in ["topic_id", "title", "raw_body"]}
     )
Пример #13
0
 def test_update_non_updatable(self, field):
     serializer = CommentSerializer(self.existing_comment,
                                    data={field: "different_value"},
                                    partial=True,
                                    context=get_context(
                                        self.course, self.request))
     self.assertFalse(serializer.is_valid())
     self.assertEqual(serializer.errors,
                      {field: ["This field is not allowed in an update."]})
Пример #14
0
 def test_update_empty_raw_body(self, value):
     serializer = CommentSerializer(self.existing_comment,
                                    data={"raw_body": value},
                                    partial=True,
                                    context=get_context(
                                        self.course, self.request))
     self.assertFalse(serializer.is_valid())
     self.assertEqual(serializer.errors,
                      {"raw_body": ["This field may not be blank."]})
Пример #15
0
 def test_update_course_id(self):
     serializer = ThreadSerializer(self.existing_thread,
                                   data={"course_id": "some/other/course"},
                                   partial=True,
                                   context=get_context(
                                       self.course, self.request))
     self.assertFalse(serializer.is_valid())
     self.assertEqual(
         serializer.errors,
         {"course_id": ["This field is not allowed in an update."]})
Пример #16
0
 def test_create_missing_field(self):
     for field in self.minimal_data:
         data = self.minimal_data.copy()
         data.pop(field)
         serializer = CommentSerializer(data=data,
                                        context=get_context(
                                            self.course, self.request,
                                            make_minimal_cs_thread()))
         assert not serializer.is_valid()
         assert serializer.errors == {field: ['This field is required.']}
Пример #17
0
 def test_create_parent_id_wrong_thread(self):
     self.register_get_comment_response({"thread_id": "different_thread", "id": "test_parent"})
     data = self.minimal_data.copy()
     data["parent_id"] = "test_parent"
     context = get_context(self.course, self.request, make_minimal_cs_thread())
     serializer = CommentSerializer(data=data, context=context)
     assert not serializer.is_valid()
     assert serializer.errors == {
         'non_field_errors': ['parent_id does not identify a comment in the thread identified by thread_id.']
     }
Пример #18
0
def create_thread(request, thread_data):
    """
    Create a thread.

    Arguments:

        request: The django request object used for build_absolute_uri and
          determining the requesting user.

        thread_data: The data for the created thread.

    Returns:

        The created thread; see discussion.rest_api.views.ThreadViewSet for more
        detail.
    """
    course_id = thread_data.get("course_id")
    user = request.user
    if not course_id:
        raise ValidationError({"course_id": ["This field is required."]})
    try:
        course_key = CourseKey.from_string(course_id)
        course = _get_course(course_key, user)
    except InvalidKeyError:
        raise ValidationError({"course_id": ["Invalid value."]})  # lint-amnesty, pylint: disable=raise-missing-from

    if not discussion_open_for_user(course, user):
        raise DiscussionBlackOutException

    context = get_context(course, request)
    _check_initializable_thread_fields(thread_data, context)
    discussion_settings = get_course_discussion_settings(course_key)
    if ("group_id" not in thread_data and is_commentable_divided(
            course_key, thread_data.get("topic_id"), discussion_settings)):
        thread_data = thread_data.copy()
        thread_data["group_id"] = get_group_id_for_user(
            user, discussion_settings)
    serializer = ThreadSerializer(data=thread_data, context=context)
    actions_form = ThreadActionsForm(thread_data)
    if not (serializer.is_valid() and actions_form.is_valid()):
        raise ValidationError(
            dict(
                list(serializer.errors.items()) +
                list(actions_form.errors.items())))
    serializer.save()
    cc_thread = serializer.instance
    thread_created.send(sender=None, user=user, post=cc_thread)
    api_thread = serializer.data
    _do_extra_actions(api_thread, cc_thread, list(thread_data.keys()),
                      actions_form, context, request)

    track_thread_created_event(request, course, cc_thread,
                               actions_form.cleaned_data["following"])

    return api_thread
Пример #19
0
 def test_update_empty_string(self, value):
     serializer = ThreadSerializer(
         self.existing_thread,
         data={field: value for field in ["topic_id", "title", "raw_body"]},
         partial=True,
         context=get_context(self.course, self.request)
     )
     assert not serializer.is_valid()
     assert serializer.errors == {
         field: ['This field may not be blank.'] for field in ['topic_id', 'title', 'raw_body']
     }
Пример #20
0
 def test_update_empty_raw_body(self, value):
     serializer = CommentSerializer(
         self.existing_comment,
         data={"raw_body": value},
         partial=True,
         context=get_context(self.course, self.request)
     )
     self.assertFalse(serializer.is_valid())
     self.assertEqual(
         serializer.errors,
         {"raw_body": ["This field may not be blank."]}
     )
Пример #21
0
 def test_update_empty_string(self, value):
     serializer = ThreadSerializer(
         self.existing_thread,
         data={field: value for field in ["topic_id", "title", "raw_body"]},
         partial=True,
         context=get_context(self.course, self.request)
     )
     self.assertFalse(serializer.is_valid())
     self.assertEqual(
         serializer.errors,
         {field: ["This field may not be blank."] for field in ["topic_id", "title", "raw_body"]}
     )
Пример #22
0
 def test_update_course_id(self):
     serializer = ThreadSerializer(
         self.existing_thread,
         data={"course_id": "some/other/course"},
         partial=True,
         context=get_context(self.course, self.request)
     )
     self.assertFalse(serializer.is_valid())
     self.assertEqual(
         serializer.errors,
         {"course_id": ["This field is not allowed in an update."]}
     )
Пример #23
0
 def test_update_non_updatable(self, field):
     serializer = CommentSerializer(
         self.existing_comment,
         data={field: "different_value"},
         partial=True,
         context=get_context(self.course, self.request)
     )
     self.assertFalse(serializer.is_valid())
     self.assertEqual(
         serializer.errors,
         {field: ["This field is not allowed in an update."]}
     )
Пример #24
0
 def test_update_empty_string(self, value):
     serializer = ThreadSerializer(
         self.existing_thread,
         data={field: value for field in ["topic_id", "title", "raw_body"]},
         partial=True,
         context=get_context(self.course, self.request)
     )
     self.assertFalse(serializer.is_valid())
     self.assertEqual(
         serializer.errors,
         {field: ["This field may not be blank."] for field in ["topic_id", "title", "raw_body"]}
     )
Пример #25
0
 def test_create_missing_field(self):
     for field in self.minimal_data:
         data = self.minimal_data.copy()
         data.pop(field)
         serializer = CommentSerializer(
             data=data,
             context=get_context(self.course, self.request, make_minimal_cs_thread())
         )
         self.assertFalse(serializer.is_valid())
         self.assertEqual(
             serializer.errors,
             {field: ["This field is required."]}
         )
Пример #26
0
 def save_and_reserialize(self, data, instance=None):
     """
     Create a serializer with the given data and (if updating) instance,
     ensure that it is valid, save the result, and return the full thread
     data from the serializer.
     """
     serializer = ThreadSerializer(instance,
                                   data=data,
                                   partial=(instance is not None),
                                   context=get_context(
                                       self.course, self.request))
     assert serializer.is_valid()
     serializer.save()
     return serializer.data
Пример #27
0
 def test_create_parent_id_nonexistent(self):
     self.register_get_comment_error_response("bad_parent", 404)
     data = self.minimal_data.copy()
     data["parent_id"] = "bad_parent"
     context = get_context(self.course, self.request,
                           make_minimal_cs_thread())
     serializer = CommentSerializer(data=data, context=context)
     self.assertFalse(serializer.is_valid())
     self.assertEqual(
         serializer.errors, {
             "non_field_errors": [
                 "parent_id does not identify a comment in the thread identified by thread_id."
             ]
         })
Пример #28
0
 def test_create_parent_id_wrong_thread(self):
     self.register_get_comment_response({"thread_id": "different_thread", "id": "test_parent"})
     data = self.minimal_data.copy()
     data["parent_id"] = "test_parent"
     context = get_context(self.course, self.request, make_minimal_cs_thread())
     serializer = CommentSerializer(data=data, context=context)
     self.assertFalse(serializer.is_valid())
     self.assertEqual(
         serializer.errors,
         {
             "non_field_errors": [
                 "parent_id does not identify a comment in the thread identified by thread_id."
             ]
         }
     )
Пример #29
0
 def save_and_reserialize(self, data, instance=None):
     """
     Create a serializer with the given data and (if updating) instance,
     ensure that it is valid, save the result, and return the full thread
     data from the serializer.
     """
     serializer = ThreadSerializer(
         instance,
         data=data,
         partial=(instance is not None),
         context=get_context(self.course, self.request)
     )
     self.assertTrue(serializer.is_valid())
     serializer.save()
     return serializer.data
Пример #30
0
 def save_and_reserialize(self, data, instance=None):
     """
     Create a serializer with the given data, ensure that it is valid, save
     the result, and return the full comment data from the serializer.
     """
     context = get_context(
         self.course, self.request,
         make_minimal_cs_thread({"course_id": str(self.course.id)}))
     serializer = CommentSerializer(instance,
                                    data=data,
                                    partial=(instance is not None),
                                    context=context)
     assert serializer.is_valid()
     serializer.save()
     return serializer.data
Пример #31
0
def create_thread(request, thread_data):
    """
    Create a thread.

    Arguments:

        request: The django request object used for build_absolute_uri and
          determining the requesting user.

        thread_data: The data for the created thread.

    Returns:

        The created thread; see discussion.rest_api.views.ThreadViewSet for more
        detail.
    """
    course_id = thread_data.get("course_id")
    user = request.user
    if not course_id:
        raise ValidationError({"course_id": ["This field is required."]})
    try:
        course_key = CourseKey.from_string(course_id)
        course = _get_course(course_key, user)
    except InvalidKeyError:
        raise ValidationError({"course_id": ["Invalid value."]})

    context = get_context(course, request)
    _check_initializable_thread_fields(thread_data, context)
    discussion_settings = get_course_discussion_settings(course_key)
    if (
            "group_id" not in thread_data and
            is_commentable_divided(course_key, thread_data.get("topic_id"), discussion_settings)
    ):
        thread_data = thread_data.copy()
        thread_data["group_id"] = get_group_id_for_user(user, discussion_settings)
    serializer = ThreadSerializer(data=thread_data, context=context)
    actions_form = ThreadActionsForm(thread_data)
    if not (serializer.is_valid() and actions_form.is_valid()):
        raise ValidationError(dict(serializer.errors.items() + actions_form.errors.items()))
    serializer.save()
    cc_thread = serializer.instance
    thread_created.send(sender=None, user=user, post=cc_thread)
    api_thread = serializer.data
    _do_extra_actions(api_thread, cc_thread, thread_data.keys(), actions_form, context, request)

    track_thread_created_event(request, course, cc_thread, actions_form.cleaned_data["following"])

    return api_thread
Пример #32
0
 def save_and_reserialize(self, data, instance=None):
     """
     Create a serializer with the given data, ensure that it is valid, save
     the result, and return the full comment data from the serializer.
     """
     context = get_context(
         self.course,
         self.request,
         make_minimal_cs_thread({"course_id": six.text_type(self.course.id)})
     )
     serializer = CommentSerializer(
         instance,
         data=data,
         partial=(instance is not None),
         context=context
     )
     self.assertTrue(serializer.is_valid())
     serializer.save()
     return serializer.data
Пример #33
0
 def serialize(self, thread):
     """
     Create a serializer with an appropriate context and use it to serialize
     the given thread, returning the result.
     """
     return ThreadSerializer(thread, context=get_context(self.course, self.request)).data
Пример #34
0
def get_thread_list(
        request,
        course_key,
        page,
        page_size,
        topic_id_list=None,
        text_search=None,
        following=False,
        view=None,
        order_by="last_activity_at",
        order_direction="desc",
        requested_fields=None,
):
    """
    Return the list of all discussion threads pertaining to the given course

    Parameters:

    request: The django request objects used for build_absolute_uri
    course_key: The key of the course to get discussion threads for
    page: The page number (1-indexed) to retrieve
    page_size: The number of threads to retrieve per page
    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
    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 default
        and only value is "desc". This will be removed in a future major
        version.
    requested_fields: Indicates which additional fields to return
        for each thread. (i.e. ['profile_image'])

    Note that topic_id_list, text_search, and following are mutually exclusive.

    Returns:

    A paginated result containing a list of threads; see
    discussion.rest_api.views.ThreadViewSet for more detail.

    Raises:

    ValidationError: if an invalid value is passed for a field.
    ValueError: if more than one of the mutually exclusive parameters is
      provided
    CourseNotFoundError: if the requesting user does not have access to the requested course
    PageNotFoundError: if page requested is beyond the last
    """
    exclusive_param_count = sum(1 for param in [topic_id_list, text_search, following] if param)
    if exclusive_param_count > 1:  # pragma: no cover
        raise ValueError("More than one mutually exclusive param passed to get_thread_list")

    cc_map = {"last_activity_at": "activity", "comment_count": "comments", "vote_count": "votes"}
    if order_by not in cc_map:
        raise ValidationError({
            "order_by":
                [u"Invalid value. '{}' must be 'last_activity_at', 'comment_count', or 'vote_count'".format(order_by)]
        })
    if order_direction != "desc":
        raise ValidationError({
            "order_direction": [u"Invalid value. '{}' must be 'desc'".format(order_direction)]
        })

    course = _get_course(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_group_id_for_user(request.user, get_course_discussion_settings(course.id))
        ),
        "page": page,
        "per_page": page_size,
        "text": text_search,
        "sort_key": cc_map.get(order_by),
    }

    if view:
        if view in ["unread", "unanswered"]:
            query_params[view] = "true"
        else:
            ValidationError({
                "view": [u"Invalid value. '{}' must be 'unread' or 'unanswered'".format(view)]
            })

    if following:
        paginated_results = context["cc_requester"].subscribed_threads(query_params)
    else:
        query_params["course_id"] = unicode(course.id)
        query_params["commentable_ids"] = ",".join(topic_id_list) if topic_id_list else None
        query_params["text"] = text_search
        paginated_results = Thread.search(query_params)
    # The comments service returns the last page of results if the requested
    # page is beyond the last page, but we want be consistent with DRF's general
    # behavior and return a PageNotFoundError in that case
    if paginated_results.page != page:
        raise PageNotFoundError("Page not found (No results on this page).")

    results = _serialize_discussion_entities(
        request, context, paginated_results.collection, requested_fields, DiscussionEntity.thread
    )

    paginator = DiscussionAPIPagination(
        request,
        paginated_results.page,
        paginated_results.num_pages,
        paginated_results.thread_count
    )
    return paginator.get_paginated_response({
        "results": results,
        "text_search_rewrite": paginated_results.corrected_text,
    })
Пример #35
0
def get_thread_list(
    request,
    course_key,
    page,
    page_size,
    topic_id_list=None,
    text_search=None,
    following=False,
    view=None,
    order_by="last_activity_at",
    order_direction="desc",
    requested_fields=None,
):
    """
    Return the list of all discussion threads pertaining to the given course

    Parameters:

    request: The django request objects used for build_absolute_uri
    course_key: The key of the course to get discussion threads for
    page: The page number (1-indexed) to retrieve
    page_size: The number of threads to retrieve per page
    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
    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 default
        and only value is "desc". This will be removed in a future major
        version.
    requested_fields: Indicates which additional fields to return
        for each thread. (i.e. ['profile_image'])

    Note that topic_id_list, text_search, and following are mutually exclusive.

    Returns:

    A paginated result containing a list of threads; see
    discussion.rest_api.views.ThreadViewSet for more detail.

    Raises:

    ValidationError: if an invalid value is passed for a field.
    ValueError: if more than one of the mutually exclusive parameters is
      provided
    CourseNotFoundError: if the requesting user does not have access to the requested course
    PageNotFoundError: if page requested is beyond the last
    """
    exclusive_param_count = sum(
        1 for param in [topic_id_list, text_search, following] if param)
    if exclusive_param_count > 1:  # pragma: no cover
        raise ValueError(
            "More than one mutually exclusive param passed to get_thread_list")

    cc_map = {
        "last_activity_at": "activity",
        "comment_count": "comments",
        "vote_count": "votes"
    }
    if order_by not in cc_map:
        raise ValidationError({
            "order_by": [
                u"Invalid value. '{}' must be 'last_activity_at', 'comment_count', or 'vote_count'"
                .format(order_by)
            ]
        })
    if order_direction != "desc":
        raise ValidationError({
            "order_direction":
            [u"Invalid value. '{}' must be 'desc'".format(order_direction)]
        })

    course = _get_course(course_key, request.user)
    context = get_context(course, request)

    query_params = {
        "user_id":
        six.text_type(request.user.id),
        "group_id":
        (None if context["is_requester_privileged"] else get_group_id_for_user(
            request.user, get_course_discussion_settings(course.id))),
        "page":
        page,
        "per_page":
        page_size,
        "text":
        text_search,
        "sort_key":
        cc_map.get(order_by),
    }

    if view:
        if view in ["unread", "unanswered"]:
            query_params[view] = "true"
        else:
            ValidationError({
                "view": [
                    u"Invalid value. '{}' must be 'unread' or 'unanswered'".
                    format(view)
                ]
            })

    if following:
        paginated_results = context["cc_requester"].subscribed_threads(
            query_params)
    else:
        query_params["course_id"] = six.text_type(course.id)
        query_params["commentable_ids"] = ",".join(
            topic_id_list) if topic_id_list else None
        query_params["text"] = text_search
        paginated_results = Thread.search(query_params)
    # The comments service returns the last page of results if the requested
    # page is beyond the last page, but we want be consistent with DRF's general
    # behavior and return a PageNotFoundError in that case
    if paginated_results.page != page:
        raise PageNotFoundError("Page not found (No results on this page).")

    results = _serialize_discussion_entities(request, context,
                                             paginated_results.collection,
                                             requested_fields,
                                             DiscussionEntity.thread)

    paginator = DiscussionAPIPagination(request, paginated_results.page,
                                        paginated_results.num_pages,
                                        paginated_results.thread_count)
    return paginator.get_paginated_response({
        "results":
        results,
        "text_search_rewrite":
        paginated_results.corrected_text,
    })