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 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.assertEqual( serializer.errors, {field: ["This field is required."] for field in ["topic_id", "title", "raw_body"]} )
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 get_thread_list(request, course_key, page, page_size, topic_id_list=None, text_search=None, following=False): """ 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 Note that topic_id_list, text_search, and following are mutually exclusive. Returns: A paginated result containing a list of threads; see discussion_api.views.ThreadViewSet for more detail. Raises: ValueError: if more than one of the mutually exclusive parameters is provided Http404: if the requesting user does not have access to the requested course or a page beyond the last is requested """ 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") course = _get_course_or_404(course_key, request.user) context = get_context(course, request) query_params = { "group_id": (None if context["is_requester_privileged"] else get_cohort_id(request.user, course.id)), "sort_key": "date", "sort_order": "desc", "page": page, "per_page": page_size, "text": text_search, } text_search_rewrite = None if following: threads, result_page, num_pages = 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 threads, result_page, num_pages, text_search_rewrite = 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 404 in that case if result_page != page: raise Http404 results = [ThreadSerializer(thread, context=context).data for thread in threads] ret = get_paginated_data(request, results, page, num_pages) ret["text_search_rewrite"] = text_search_rewrite return ret
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 Http404 if the thread does not exist or the user cannot access it. """ retrieve_kwargs = retrieve_kwargs or {} try: 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_or_404(course_key, request.user) context = get_context(course, request, cc_thread) if ( not context["is_requester_privileged"] and cc_thread["group_id"] and is_commentable_cohorted(course.id, cc_thread["commentable_id"]) ): requester_cohort = get_cohort_id(request.user, course.id) if requester_cohort is not None and cc_thread["group_id"] != requester_cohort: raise Http404 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 Http404
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
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.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.assertEqual(serializer.errors, {"raw_body": ["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.assertEqual(serializer.errors, {"raw_body": ["This field is required."]})
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.assertEqual(serializer.errors, {field: ["This field is not allowed in an update."]})
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.assertEqual(serializer.errors, {"course_id": ["This field is not allowed in an update."]})
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_course_id(self): serializer = ThreadSerializer(self.existing_thread, data={"course_id": "some/other/course"}, partial=True, context=get_context( self.course, self.request)) self.assertEqual( serializer.errors, {"course_id": ["This field is not allowed in an update."]})
def save_and_reserialize(self, data): """ Create a serializer with the given data, ensure that it is valid, save the result, and return the full thread data from the serializer. """ serializer = ThreadSerializer(data=data, context=get_context(self.course, self.request)) self.assertTrue(serializer.is_valid()) serializer.save() return serializer.data
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_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.assertEqual( serializer.errors, {field: ["This field is required."] for field in ["topic_id", "title", "raw_body"]} )
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_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.assertEqual( serializer.errors, {field: ["This field is required."] for field in ["topic_id", "title", "raw_body"]} )
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 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_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_or_404(course_key, user) except (Http404, InvalidKeyError): raise ValidationError({"course_id": ["Invalid value."]}) context = get_context(course, request) _check_initializable_thread_fields(thread_data, context) if ( "group_id" not in thread_data and is_commentable_cohorted(course_key, thread_data.get("topic_id")) ): thread_data = thread_data.copy() thread_data["group_id"] = get_cohort_id(user, course_key) 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.object 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) track_forum_event( request, THREAD_CREATED_EVENT_NAME, course, cc_thread, get_thread_created_event_data(cc_thread, followed=actions_form.cleaned_data["following"]) ) return api_thread
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
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_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 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
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_thread(request, thread_data): """ Create a thread. Parameters: 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_api.views.ThreadViewSet for more detail. """ course_id = thread_data.get("course_id") if not course_id: raise ValidationError({"course_id": ["This field is required."]}) try: course_key = CourseLocator.from_string(course_id) course = _get_course_or_404(course_key, request.user) except (Http404, InvalidKeyError): raise ValidationError({"course_id": ["Invalid value."]}) context = get_context(course, request) serializer = ThreadSerializer(data=thread_data, context=context) extras_form = ThreadCreateExtrasForm(thread_data) if not (serializer.is_valid() and extras_form.is_valid()): raise ValidationError(dict(serializer.errors.items() + extras_form.errors.items())) serializer.save() thread = serializer.object ret = serializer.data following = extras_form.cleaned_data["following"] if following: context["cc_requester"].follow(thread) ret["following"] = True track_forum_event( request, THREAD_CREATED_EVENT_NAME, course, thread, get_thread_created_event_data(thread, followed=following) ) return serializer.data
def get_thread_list(request, course_key, page, page_size): """ 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 Returns: A paginated result containing a list of threads; see discussion_api.views.ThreadViewSet for more detail. """ course = _get_course_or_404(course_key, request.user) context = get_context(course, request) threads, result_page, num_pages, _ = Thread.search({ "course_id": unicode(course.id), "group_id": (None if context["is_requester_privileged"] else get_cohort_id( request.user, course.id)), "sort_key": "date", "sort_order": "desc", "page": page, "per_page": page_size, }) # 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 result_page != page: raise Http404 results = [ ThreadSerializer(thread, context=context).data for thread in threads ] return get_paginated_data(request, results, page, num_pages)
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 get_thread_list(request, course_key, page, page_size, topic_id_list=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 Returns: A paginated result containing a list of threads; see discussion_api.views.ThreadViewSet for more detail. """ course = _get_course_or_404(course_key, request.user) context = get_context(course, request) topic_ids_csv = ",".join(topic_id_list) if topic_id_list else None threads, result_page, num_pages, _ = Thread.search({ "course_id": unicode(course.id), "group_id": ( None if context["is_requester_privileged"] else get_cohort_id(request.user, course.id) ), "sort_key": "date", "sort_order": "desc", "page": page, "per_page": page_size, "commentable_ids": topic_ids_csv, }) # 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 result_page != page: raise Http404 results = [ThreadSerializer(thread, context=context).data for thread in threads] return get_paginated_data(request, results, page, num_pages)
def serialize(self, comment): """ Create a serializer with an appropriate context and use it to serialize the given comment, returning the result. """ return CommentSerializer(comment, context=get_context(self.course, self.user)).data
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_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": ["Invalid value. '{}' must be 'last_activity_at', 'comment_count', or 'vote_count'".format(order_by)] }) if order_direction != "desc": raise ValidationError({ "order_direction": ["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_cohort_id(request.user, 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": ["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_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.user) # 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_thread_list(request, course_key, page, page_size, topic_id_list=None, text_search=None, following=False): """ 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 Note that topic_id_list, text_search, and following are mutually exclusive. Returns: A paginated result containing a list of threads; see discussion_api.views.ThreadViewSet for more detail. Raises: ValueError: if more than one of the mutually exclusive parameters is provided Http404: if the requesting user does not have access to the requested course or a page beyond the last is requested """ 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") course = _get_course_or_404(course_key, request.user) context = get_context(course, request) query_params = { "group_id": (None if context["is_requester_privileged"] else get_cohort_id( request.user, course.id)), "sort_key": "date", "sort_order": "desc", "page": page, "per_page": page_size, "text": text_search, } text_search_rewrite = None if following: threads, result_page, num_pages = 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 threads, result_page, num_pages, text_search_rewrite = 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 404 in that case if result_page != page: raise Http404 results = [ ThreadSerializer(thread, context=context).data for thread in threads ] ret = get_paginated_data(request, results, page, num_pages) ret["text_search_rewrite"] = text_search_rewrite return ret
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", ): """ 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 only values are "asc" or "desc". The default is "desc". Note that topic_id_list, text_search, and following are mutually exclusive. Returns: A paginated result containing a list of threads; see discussion_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 Http404: if the requesting user does not have access to the requested course or a page beyond the last is requested """ 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": "date", "comment_count": "comments", "vote_count": "votes"} if order_by not in cc_map: raise ValidationError({ "order_by": ["Invalid value. '{}' must be 'last_activity_at', 'comment_count', or 'vote_count'".format(order_by)] }) if order_direction not in ["asc", "desc"]: raise ValidationError({ "order_direction": ["Invalid value. '{}' must be 'asc' or 'desc'".format(order_direction)] }) course = _get_course_or_404(course_key, request.user) context = get_context(course, request) query_params = { "user_id": unicode(request.user.id), "group_id": ( None if context["is_requester_privileged"] else get_cohort_id(request.user, course.id) ), "page": page, "per_page": page_size, "text": text_search, "sort_key": cc_map.get(order_by), "sort_order": order_direction, } text_search_rewrite = None if view: if view in ["unread", "unanswered"]: query_params[view] = "true" else: ValidationError({ "view": ["Invalid value. '{}' must be 'unread' or 'unanswered'".format(view)] }) if following: threads, result_page, num_pages = 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 threads, result_page, num_pages, text_search_rewrite = 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 404 in that case if result_page != page: raise Http404 results = [ThreadSerializer(thread, context=context).data for thread in threads] ret = get_paginated_data(request, results, page, num_pages) ret["text_search_rewrite"] = text_search_rewrite return ret
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_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": [ "Invalid value. '{}' must be 'last_activity_at', 'comment_count', or 'vote_count'" .format(order_by) ] }) if order_direction != "desc": raise ValidationError({ "order_direction": ["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_cohort_id( request.user, 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": [ "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_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)