Exemplo n.º 1
0
def _handle_pinned_field(pin_thread: bool, cc_content: Thread, user: User):
    """
    Pins or unpins a thread

    Arguments:

        pin_thread (bool): Value of field from API
        cc_content (Thread): The thread on which to operate
        user (User): The user performing the action
    """
    if pin_thread:
        cc_content.pin(user, cc_content.id)
    else:
        cc_content.un_pin(user, cc_content.id)
Exemplo n.º 2
0
 def setUp(self):
     super().setUp()
     httpretty.reset()
     httpretty.enable()
     self.addCleanup(httpretty.reset)
     self.addCleanup(httpretty.disable)
     self.user = UserFactory.create()
     self.register_get_user_response(self.user)
     self.request = RequestFactory().get("/dummy")
     self.request.user = self.user
     self.minimal_data = {
         "course_id": str(self.course.id),
         "topic_id": "test_topic",
         "type": "discussion",
         "title": "Test Title",
         "raw_body": "Test body",
     }
     self.existing_thread = Thread(
         **make_minimal_cs_thread({
             "id": "existing_thread",
             "course_id": str(self.course.id),
             "commentable_id": "original_topic",
             "thread_type": "discussion",
             "title": "Original Title",
             "body": "Original body",
             "user_id": str(self.user.id),
             "username": self.user.username,
             "read": "False",
             "endorsed": "False"
         }))
Exemplo n.º 3
0
 def test_comment(
     self,
     is_author,
     is_thread_author,
     is_privileged,
     allow_anonymous,
     allow_anonymous_to_peers,
     has_parent,
     thread_type
 ):
     comment = Comment(
         user_id="5" if is_author else "6",
         type="comment",
         parent_id="parent-id" if has_parent else None,
     )
     context = _get_context(
         requester_id="5",
         is_requester_privileged=is_privileged,
         thread=Thread(user_id="5" if is_thread_author else "6", thread_type=thread_type),
         allow_anonymous=allow_anonymous,
         allow_anonymous_to_peers=allow_anonymous_to_peers,
     )
     actual = get_editable_fields(comment, context)
     expected = {"abuse_flagged", "voted"}
     if is_privileged:
         expected |= {"edit_reason_code"}
     if is_author or is_privileged:
         expected |= {"raw_body"}
     if not has_parent and ((is_thread_author and thread_type == "question") or is_privileged):
         expected |= {"endorsed"}
     if is_author and allow_anonymous:
         expected |= {"anonymous"}
     if is_author and allow_anonymous_to_peers:
         expected |= {"anonymous_to_peers"}
     assert actual == expected
Exemplo n.º 4
0
 def test_thread(
     self,
     is_author,
     is_privileged,
     is_cohorted,
     allow_anonymous,
     allow_anonymous_to_peers
 ):
     thread = Thread(user_id="5" if is_author else "6", type="thread")
     context = _get_context(
         requester_id="5",
         is_requester_privileged=is_privileged,
         is_cohorted=is_cohorted,
         allow_anonymous=allow_anonymous,
         allow_anonymous_to_peers=allow_anonymous_to_peers,
     )
     actual = get_editable_fields(thread, context)
     expected = {"abuse_flagged", "following", "read", "voted"}
     if is_privileged:
         expected |= {"closed", "pinned", "close_reason_code", "edit_reason_code"}
     if is_author or is_privileged:
         expected |= {"topic_id", "type", "title", "raw_body"}
     if is_privileged and is_cohorted:
         expected |= {"group_id"}
     if is_author and allow_anonymous:
         expected |= {"anonymous"}
     if is_author and allow_anonymous_to_peers:
         expected |= {"anonymous_to_peers"}
     assert actual == expected
Exemplo n.º 5
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.")
Exemplo n.º 6
0
 def test_comment(self, is_author, is_thread_author, is_privileged):
     comment = Comment(user_id="5" if is_author else "6")
     context = _get_context(
         requester_id="5",
         is_requester_privileged=is_privileged,
         thread=Thread(user_id="5" if is_thread_author else "6"))
     assert can_delete(comment, context) == (is_author or is_privileged)
Exemplo n.º 7
0
def get_initializable_thread_fields(context):
    """
    Return the set of fields that the requester can initialize for a thread

    Any field that is editable by the author should also be initializable.
    """
    ret = get_editable_fields(
        Thread(user_id=context["cc_requester"]["id"], type="thread"), context)
    ret |= NON_UPDATABLE_THREAD_FIELDS
    return ret
 def test_thread(self, is_author, is_privileged, is_cohorted):
     thread = Thread(user_id="5" if is_author else "6", type="thread")
     context = _get_context(requester_id="5",
                            is_requester_privileged=is_privileged,
                            is_cohorted=is_cohorted)
     actual = get_editable_fields(thread, context)
     expected = {"abuse_flagged", "following", "read", "voted"}
     if is_author or is_privileged:
         expected |= {"topic_id", "type", "title", "raw_body"}
     if is_privileged and is_cohorted:
         expected |= {"group_id"}
     self.assertEqual(actual, expected)
 def test_comment(self, is_thread_author, thread_type, is_privileged):
     context = _get_context(requester_id="5",
                            is_requester_privileged=is_privileged,
                            thread=Thread(
                                user_id="5" if is_thread_author else "6",
                                thread_type=thread_type))
     actual = get_initializable_comment_fields(context)
     expected = {
         "abuse_flagged", "parent_id", "raw_body", "thread_id", "voted"
     }
     if (is_thread_author and thread_type == "question") or is_privileged:
         expected |= {"endorsed"}
     self.assertEqual(actual, expected)
 def test_comment(self, is_author, is_thread_author, thread_type, is_privileged):
     comment = Comment(user_id="5" if is_author else "6", type="comment")
     context = _get_context(
         requester_id="5",
         is_requester_privileged=is_privileged,
         thread=Thread(user_id="5" if is_thread_author else "6", thread_type=thread_type)
     )
     actual = get_editable_fields(comment, context)
     expected = {"abuse_flagged", "voted"}
     if is_author or is_privileged:
         expected |= {"raw_body"}
     if (is_thread_author and thread_type == "question") or is_privileged:
         expected |= {"endorsed"}
     assert actual == expected
Exemplo n.º 11
0
 def create(self, validated_data):
     thread = Thread(user_id=self.context["cc_requester"]["id"],
                     **validated_data)
     thread.save()
     return thread
Exemplo n.º 12
0
 def create(self, validated_data):
     thread = Thread(user_id=self.context["cc_requester"]["id"], **validated_data)
     thread.save()
     return thread
Exemplo n.º 13
0
 def test_thread(self, is_author, is_privileged):
     thread = Thread(user_id="5" if is_author else "6")
     context = _get_context(requester_id="5", is_requester_privileged=is_privileged)
     assert can_delete(thread, context) == (is_author or is_privileged)
Exemplo n.º 14
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,
    })
Exemplo n.º 15
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,
    })
def get_thread_list(
    request: Request,
    course_key: CourseKey,
    page: int,
    page_size: int,
    topic_id_list: List[str] = None,
    text_search: Optional[str] = None,
    following: Optional[bool] = False,
    author: Optional[str] = None,
    thread_type: Optional[ThreadType] = None,
    flagged: Optional[bool] = None,
    view: Optional[ViewType] = None,
    order_by: ThreadOrderingType = "last_activity_at",
    order_direction: Literal["desc"] = "desc",
    requested_fields: Optional[List[Literal["profile_image"]]] = None,
    count_flagged: bool = 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
    count_flagged: If true, fetch the count of flagged items in each thread
    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
    author: If provided, retrieve only threads by this author
    thread_type: filter for "discussion" or "question threads
    flagged: filter for only threads that are flagged
    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:

    PermissionDenied: If count_flagged is set but the user isn't privileged
    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":
                [f"Invalid value. '{order_by}' must be 'last_activity_at', 'comment_count', or 'vote_count'"]
        })
    if order_direction != "desc":
        raise ValidationError({
            "order_direction": [f"Invalid value. '{order_direction}' must be 'desc'"]
        })

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

    author_id = None
    if author:
        try:
            author_id = User.objects.get(username=author).id
        except User.DoesNotExist:
            # Raising an error for a missing user leaks the presence of a username,
            # so just return an empty response.
            return DiscussionAPIPagination(request, 0, 1).get_paginated_response({
                "results": [],
                "text_search_rewrite": None,
            })

    if count_flagged and not context["is_requester_privileged"]:
        raise PermissionDenied("`count_flagged` can only be set by users with moderator access or higher.")

    query_params = {
        "user_id": str(request.user.id),
        "group_id": (
            None if context["is_requester_privileged"] else
            get_group_id_for_user(request.user, CourseDiscussionSettings.get(course.id))
        ),
        "page": page,
        "per_page": page_size,
        "text": text_search,
        "sort_key": cc_map.get(order_by),
        "author_id": author_id,
        "flagged": flagged,
        "thread_type": thread_type,
        "count_flagged": count_flagged,
    }

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

    if following:
        paginated_results = context["cc_requester"].subscribed_threads(query_params)
    else:
        query_params["course_id"] = str(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,
    })