Example #1
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."]})
 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."]})
 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."]})
Example #4
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."]})
 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."]})
 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": unicode(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
Example #7
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."]})
 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."]},
     )
 def test_parent_id_nonexistent(self):
     self.register_get_comment_error_response("bad_parent", 404)
     context = get_context(self.course, self.request, make_minimal_cs_thread(), "bad_parent")
     serializer = CommentSerializer(data=self.minimal_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."
             ]
         }
     )
Example #10
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."
             ]
         })
Example #11
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": unicode(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
Example #12
0
def create_comment(request, comment_data):
    """
    Create a comment.

    Arguments:

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

        comment_data: The data for the created comment.

    Returns:

        The created comment; see discussion_api.views.CommentViewSet for more
        detail.
    """
    thread_id = comment_data.get("thread_id")
    if not thread_id:
        raise ValidationError({"thread_id": ["This field is required."]})
    try:
        cc_thread, context = _get_thread_and_context(request, thread_id)
    except Http404:
        raise ValidationError({"thread_id": ["Invalid value."]})

    # if a thread is closed; no new comments could be made to it
    if cc_thread['closed']:
        raise PermissionDenied

    _check_initializable_comment_fields(comment_data, context)
    serializer = CommentSerializer(data=comment_data, context=context)
    actions_form = CommentActionsForm(comment_data)
    if not (serializer.is_valid() and actions_form.is_valid()):
        raise ValidationError(
            dict(serializer.errors.items() + actions_form.errors.items()))
    serializer.save()
    cc_comment = serializer.instance
    comment_created.send(sender=None, user=request.user, post=cc_comment)
    api_comment = serializer.data
    _do_extra_actions(api_comment, cc_comment, comment_data.keys(),
                      actions_form, context)

    track_comment_created_event(request,
                                context["course"],
                                cc_comment,
                                cc_thread["commentable_id"],
                                followed=False)

    return api_comment
Example #13
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
Example #14
0
def update_comment(request, comment_id, update_data):
    """
    Update a comment.

    Arguments:

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

        comment_id: The id for the comment to update.

        update_data: The data to update in the comment.

    Returns:

        The updated comment; see discussion_api.views.CommentViewSet for more
        detail.

    Raises:

        CommentNotFoundError: if the comment does not exist or is not accessible
        to the requesting user

        PermissionDenied: if the comment is accessible to but not editable by
          the requesting user

        ValidationError: if there is an error applying the update (e.g. raw_body
          is empty or thread_id is included)
    """
    cc_comment, context = _get_comment_and_context(request, comment_id)
    _check_editable_fields(cc_comment, update_data, context)
    serializer = CommentSerializer(cc_comment,
                                   data=update_data,
                                   partial=True,
                                   context=context)
    actions_form = CommentActionsForm(update_data)
    if not (serializer.is_valid() and actions_form.is_valid()):
        raise ValidationError(
            dict(serializer.errors.items() + actions_form.errors.items()))
    # Only save comment object if some of the edited fields are in the comment data, not extra actions
    if set(update_data) - set(actions_form.fields):
        serializer.save()
        comment_edited.send(sender=None, user=request.user, post=cc_comment)
    api_comment = serializer.data
    _do_extra_actions(api_comment, cc_comment, update_data.keys(),
                      actions_form, context, request)
    return api_comment
Example #15
0
def create_comment(request, comment_data):
    """
    Create a comment.

    Arguments:

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

        comment_data: The data for the created comment.

    Returns:

        The created comment; see discussion_api.views.CommentViewSet for more
        detail.
    """
    thread_id = comment_data.get("thread_id")
    if not thread_id:
        raise ValidationError({"thread_id": ["This field is required."]})
    try:
        cc_thread, context = _get_thread_and_context(request, thread_id)
    except Http404:
        raise ValidationError({"thread_id": ["Invalid value."]})

    _check_initializable_comment_fields(comment_data, context)
    serializer = CommentSerializer(data=comment_data, context=context)
    actions_form = CommentActionsForm(comment_data)
    if not (serializer.is_valid() and actions_form.is_valid()):
        raise ValidationError(dict(serializer.errors.items() + actions_form.errors.items()))
    serializer.save()
    cc_comment = serializer.object
    comment_created.send(sender=None, user=request.user, post=cc_comment)
    api_comment = serializer.data
    _do_extra_actions(api_comment, cc_comment, comment_data.keys(), actions_form, context)

    track_forum_event(
        request,
        get_comment_created_event_name(cc_comment),
        context["course"],
        cc_comment,
        get_comment_created_event_data(cc_comment, cc_thread["commentable_id"], followed=False)
    )

    return api_comment
Example #16
0
def create_comment(request, comment_data):
    """
    Create a comment.

    Parameters:

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

        comment_data: The data for the created comment.

    Returns:

        The created comment; see discussion_api.views.CommentViewSet for more
        detail.
    """
    thread_id = comment_data.get("thread_id")
    if not thread_id:
        raise ValidationError({"thread_id": ["This field is required."]})
    try:
        cc_thread, context = _get_thread_and_context(request, thread_id)
    except Http404:
        raise ValidationError({"thread_id": ["Invalid value."]})

    serializer = CommentSerializer(data=comment_data, context=context)
    actions_form = CommentActionsForm(comment_data)
    if not (serializer.is_valid() and actions_form.is_valid()):
        raise ValidationError(
            dict(serializer.errors.items() + actions_form.errors.items()))
    serializer.save()

    cc_comment = serializer.object
    api_comment = serializer.data
    _do_extra_actions(api_comment, cc_comment, comment_data.keys(),
                      actions_form, context)

    track_forum_event(
        request, get_comment_created_event_name(cc_comment), context["course"],
        cc_comment,
        get_comment_created_event_data(cc_comment,
                                       cc_thread["commentable_id"],
                                       followed=False))

    return api_comment
Example #17
0
def create_comment(request, comment_data):
    """
    Create a comment.

    Parameters:

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

        comment_data: The data for the created comment.

    Returns:

        The created comment; see discussion_api.views.CommentViewSet for more
        detail.
    """
    thread_id = comment_data.get("thread_id")
    if not thread_id:
        raise ValidationError({"thread_id": ["This field is required."]})
    try:
        thread = Thread(id=thread_id).retrieve(mark_as_read=False)
        course_key = CourseLocator.from_string(thread["course_id"])
        course = _get_course_or_404(course_key, request.user)
    except (Http404, CommentClientRequestError):
        raise ValidationError({"thread_id": ["Invalid value."]})

    parent_id = comment_data.get("parent_id")
    context = get_context(course, request, thread, parent_id)
    serializer = CommentSerializer(data=comment_data, context=context)
    if not serializer.is_valid():
        raise ValidationError(serializer.errors)
    serializer.save()

    comment = serializer.object
    track_forum_event(
        request,
        get_comment_created_event_name(comment),
        course,
        comment,
        get_comment_created_event_data(comment, thread["commentable_id"], followed=False)
    )

    return serializer.data
Example #18
0
def update_comment(request, comment_id, update_data):
    """
    Update a comment.

    Arguments:

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

        comment_id: The id for the comment to update.

        update_data: The data to update in the comment.

    Returns:

        The updated comment; see discussion_api.views.CommentViewSet for more
        detail.

    Raises:

        Http404: if the comment does not exist or is not accessible to the
          requesting user

        PermissionDenied: if the comment is accessible to but not editable by
          the requesting user

        ValidationError: if there is an error applying the update (e.g. raw_body
          is empty or thread_id is included)
    """
    cc_comment, context = _get_comment_and_context(request, comment_id)
    _check_editable_fields(cc_comment, update_data, context)
    serializer = CommentSerializer(cc_comment, data=update_data, partial=True, context=context)
    actions_form = CommentActionsForm(update_data)
    if not (serializer.is_valid() and actions_form.is_valid()):
        raise ValidationError(dict(serializer.errors.items() + actions_form.errors.items()))
    # Only save comment object if some of the edited fields are in the comment data, not extra actions
    if set(update_data) - set(actions_form.fields):
        serializer.save()
        comment_edited.send(sender=None, user=request.user, post=cc_comment)
    api_comment = serializer.data
    _do_extra_actions(api_comment, cc_comment, update_data.keys(), actions_form, context)
    return api_comment
Example #19
0
def get_response_comments(request, comment_id, page, page_size):
    """
    Return the list of comments for the given thread response.

    Arguments:

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

        comment_id: The id of the comment/response to get child comments for.

        page: The page number (1-indexed) to retrieve

        page_size: The number of comments to retrieve per page

    Returns:

        A paginated result containing a list of comments

    """
    try:
        cc_comment = Comment(id=comment_id).retrieve()
        cc_thread, context = _get_thread_and_context(request,
                                                     cc_comment["thread_id"],
                                                     retrieve_kwargs={
                                                         "recursive": True,
                                                     })
        if cc_thread["thread_type"] == "question":
            thread_responses = itertools.chain(
                cc_thread["endorsed_responses"],
                cc_thread["non_endorsed_responses"])
        else:
            thread_responses = cc_thread["children"]
        response_comments = []
        for response in thread_responses:
            if response["id"] == comment_id:
                response_comments = response["children"]
                break

        response_skip = page_size * (page - 1)
        paged_response_comments = response_comments[response_skip:(
            response_skip + page_size)]
        results = [
            CommentSerializer(comment, context=context).data
            for comment in paged_response_comments
        ]

        comments_count = len(response_comments)
        num_pages = (comments_count + page_size -
                     1) / page_size if comments_count else 1
        return get_paginated_data(request, results, page, num_pages)
    except CommentClientRequestError:
        raise Http404
Example #20
0
 def test_create_parent_id_too_deep(self, max_depth):
     with mock.patch("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."]})
Example #21
0
def create_comment(request, comment_data):
    """
    Create a comment.

    Parameters:

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

        comment_data: The data for the created comment.

    Returns:

        The created comment; see discussion_api.views.CommentViewSet for more
        detail.
    """
    thread_id = comment_data.get("thread_id")
    parent_id = comment_data.get("parent_id")
    if not thread_id:
        raise ValidationError({"thread_id": ["This field is required."]})
    try:
        cc_thread, context = _get_thread_and_context(request, thread_id, parent_id)
    except Http404:
        raise ValidationError({"thread_id": ["Invalid value."]})

    serializer = CommentSerializer(data=comment_data, context=context)
    if not serializer.is_valid():
        raise ValidationError(serializer.errors)
    serializer.save()

    cc_comment = serializer.object
    track_forum_event(
        request,
        get_comment_created_event_name(cc_comment),
        context["course"],
        cc_comment,
        get_comment_created_event_data(cc_comment, cc_thread["commentable_id"], followed=False)
    )

    return serializer.data
Example #22
0
File: api.py Project: rhndg/openedx
def update_comment(request, comment_id, update_data):
    """
    Update a comment.

    Parameters:

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

        comment_id: The id for the comment to update.

        update_data: The data to update in the comment.

    Returns:

        The updated comment; see discussion_api.views.CommentViewSet for more
        detail.

    Raises:

        Http404: if the comment does not exist or is not accessible to the
          requesting user

        PermissionDenied: if the comment is accessible to but not editable by
          the requesting user

        ValidationError: if there is an error applying the update (e.g. raw_body
          is empty or thread_id is included)
    """
    cc_comment, context = _get_comment_and_context(request, comment_id)
    if not _is_user_author_or_privileged(cc_comment, context):
        raise PermissionDenied()
    serializer = CommentSerializer(cc_comment, data=update_data, partial=True, context=context)
    if not serializer.is_valid():
        raise ValidationError(serializer.errors)
    # Only save comment object if the comment is actually modified
    if update_data:
        serializer.save()
    return serializer.data
Example #23
0
def _serialize_discussion_entities(request, context, discussion_entities,
                                   requested_fields, discussion_entity_type):
    """
    It serializes Discussion Entity (Thread or Comment) and add additional data if requested.

    For a given list of Thread/Comment; it serializes and add additional information to the
    object as per requested_fields list (i.e. profile_image).

    Parameters:

        request: The django request object
        context: The context appropriate for use with the thread or comment
        discussion_entities: List of Thread or Comment objects
        requested_fields: Indicates which additional fields to return
            for each thread.
        discussion_entity_type: DiscussionEntity Enum value for Thread or Comment

    Returns:

        A list of serialized discussion entities
    """
    results = []
    usernames = []
    include_profile_image = _include_profile_image(requested_fields)
    for entity in discussion_entities:
        if discussion_entity_type == DiscussionEntity.thread:
            serialized_entity = ThreadSerializer(entity, context=context).data
        elif discussion_entity_type == DiscussionEntity.comment:
            serialized_entity = CommentSerializer(entity, context=context).data
        results.append(serialized_entity)

        if include_profile_image:
            if serialized_entity['author'] and serialized_entity[
                    'author'] not in usernames:
                usernames.append(serialized_entity['author'])
            if ('endorsed' in serialized_entity
                    and serialized_entity['endorsed']
                    and 'endorsed_by' in serialized_entity
                    and serialized_entity['endorsed_by']
                    and serialized_entity['endorsed_by'] not in usernames):
                usernames.append(serialized_entity['endorsed_by'])

    results = _add_additional_response_fields(request, results, usernames,
                                              discussion_entity_type,
                                              include_profile_image)
    return results
Example #24
0
def get_comment_list(request, thread_id, endorsed, page, page_size):
    """
    Return the list of comments in the given thread.

    Parameters:

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

        thread_id: The id of the thread to get comments for.

        endorsed: Boolean indicating whether to get endorsed or non-endorsed
          comments (or None for all comments). Must be None for a discussion
          thread and non-None for a question thread.

        page: The page number (1-indexed) to retrieve

        page_size: The number of comments to retrieve per page

    Returns:

        A paginated result containing a list of comments; see
        discussion_api.views.CommentViewSet for more detail.
    """
    response_skip = page_size * (page - 1)
    try:
        cc_thread = Thread(id=thread_id).retrieve(
            recursive=True,
            user_id=request.user.id,
            mark_as_read=True,
            response_skip=response_skip,
            response_limit=page_size
        )
    except CommentClientRequestError:
        # page and page_size are validated at a higher level, so the only
        # possible request error is if the thread doesn't exist
        raise Http404

    course_key = CourseLocator.from_string(cc_thread["course_id"])
    course = _get_course_or_404(course_key, request.user)
    context = get_context(course, request, cc_thread)

    # Ensure user has access to the thread
    if not context["is_requester_privileged"] and cc_thread["group_id"]:
        requester_cohort = get_cohort_id(request.user, course_key)
        if requester_cohort is not None and cc_thread["group_id"] != requester_cohort:
            raise Http404

    # Responses to discussion threads cannot be separated by endorsed, but
    # responses to question threads must be separated by endorsed due to the
    # existing comments service interface
    if cc_thread["thread_type"] == "question":
        if endorsed is None:
            raise ValidationError({"endorsed": ["This field is required for question threads."]})
        elif endorsed:
            # CS does not apply resp_skip and resp_limit to endorsed responses
            # of a question post
            responses = cc_thread["endorsed_responses"][response_skip:(response_skip + page_size)]
            resp_total = len(cc_thread["endorsed_responses"])
        else:
            responses = cc_thread["non_endorsed_responses"]
            resp_total = cc_thread["non_endorsed_resp_total"]
    else:
        if endorsed is not None:
            raise ValidationError(
                {"endorsed": ["This field may not be specified for discussion threads."]}
            )
        responses = cc_thread["children"]
        resp_total = cc_thread["resp_total"]

    # 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 404 in that case
    if not responses and page != 1:
        raise Http404
    num_pages = (resp_total + page_size - 1) / page_size if resp_total else 1

    results = [CommentSerializer(response, context=context).data for response in responses]
    return get_paginated_data(request, results, page, num_pages)
Example #25
0
def get_comment_list(request, thread_id, endorsed, page, page_size, mark_as_read=False):
    """
    Return the list of comments in the given thread.

    Arguments:

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

        thread_id: The id of the thread to get comments for.

        endorsed: Boolean indicating whether to get endorsed or non-endorsed
          comments (or None for all comments). Must be None for a discussion
          thread and non-None for a question thread.

        page: The page number (1-indexed) to retrieve

        page_size: The number of comments to retrieve per page

        mark_as_read: Marks the thread of the comment list as read.

    Returns:

        A paginated result containing a list of comments; see
        discussion_api.views.CommentViewSet for more detail.
    """
    response_skip = page_size * (page - 1)
    cc_thread, context = _get_thread_and_context(
        request,
        thread_id,
        retrieve_kwargs={
            "recursive": True,
            "user_id": request.user.id,
            "mark_as_read": mark_as_read,
            "response_skip": response_skip,
            "response_limit": page_size,
        }
    )

    # Responses to discussion threads cannot be separated by endorsed, but
    # responses to question threads must be separated by endorsed due to the
    # existing comments service interface
    if cc_thread["thread_type"] == "question":
        if endorsed is None:
            raise ValidationError({"endorsed": ["This field is required for question threads."]})
        elif endorsed:
            # CS does not apply resp_skip and resp_limit to endorsed responses
            # of a question post
            responses = cc_thread["endorsed_responses"][response_skip:(response_skip + page_size)]
            resp_total = len(cc_thread["endorsed_responses"])
        else:
            responses = cc_thread["non_endorsed_responses"]
            resp_total = cc_thread["non_endorsed_resp_total"]
    else:
        if endorsed is not None:
            raise ValidationError(
                {"endorsed": ["This field may not be specified for discussion threads."]}
            )
        responses = cc_thread["children"]
        resp_total = cc_thread["resp_total"]

    # 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 404 in that case
    if not responses and page != 1:
        raise Http404
    num_pages = (resp_total + page_size - 1) / page_size if resp_total else 1

    results = [CommentSerializer(response, context=context).data for response in responses]
    return get_paginated_data(request, results, page, num_pages)