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."]})
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
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." ] } )
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." ] })
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
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
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
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
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
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
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
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
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
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."]})
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
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
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
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)
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)