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 do_case(self, objects, page_num, num_pages, expected): """ Make a dummy request, and assert that get_paginated_data with the given parameters returns the expected result """ request = RequestFactory().get("/test") actual = get_paginated_data(request, objects, page_num, num_pages) self.assertEqual(actual, expected)
def get_thread_list(request, course, 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: 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. """ user_is_privileged = Role.objects.filter( course_id=course.id, name__in=[FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA], users=request.user ).exists() cc_user = User.from_django_user(request.user).retrieve() threads, result_page, num_pages, _ = Thread.search({ "course_id": unicode(course.id), "group_id": None if user_is_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 # TODO: cache staff_user_ids and ta_user_ids if we need to improve perf staff_user_ids = { user.id for role in Role.objects.filter( name__in=[FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR], course_id=course.id ) for user in role.users.all() } ta_user_ids = { user.id for role in Role.objects.filter(name=FORUM_ROLE_COMMUNITY_TA, course_id=course.id) for user in role.users.all() } # For now, the only groups are cohorts group_ids_to_names = get_cohort_names(course) results = [ _cc_thread_to_api_thread(thread, cc_user, staff_user_ids, ta_user_ids, group_ids_to_names) for thread in threads ] return get_paginated_data(request, results, page, num_pages)
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 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 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. """ user_is_privileged = Role.objects.filter(course_id=course_key, name__in=[ FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA ], users=request.user).exists() threads, result_page, num_pages, _ = Thread.search({ "course_id": unicode(course_key), "group_id": None if user_is_privileged else get_cohort_id(request.user, course_key), "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 = [_cc_thread_to_api_thread(thread) for thread in threads] return get_paginated_data(request, results, page, num_pages)
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 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 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. """ user_is_privileged = Role.objects.filter( course_id=course_key, name__in=[FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA], users=request.user ).exists() threads, result_page, num_pages, _ = Thread.search({ "course_id": unicode(course_key), "group_id": None if user_is_privileged else get_cohort_id(request.user, course_key), "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 = [_cc_thread_to_api_thread(thread) for thread in threads] 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)
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_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_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_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