Commit e723fb6a by wajeeha-khalid

MA-1930 add result count in paginated endpoints

parent df649ba2
...@@ -328,24 +328,29 @@ def get_thread_list( ...@@ -328,24 +328,29 @@ def get_thread_list(
}) })
if following: if following:
threads, result_page, num_pages = context["cc_requester"].subscribed_threads(query_params) paginated_results = context["cc_requester"].subscribed_threads(query_params)
else: else:
query_params["course_id"] = unicode(course.id) query_params["course_id"] = unicode(course.id)
query_params["commentable_ids"] = ",".join(topic_id_list) if topic_id_list else None query_params["commentable_ids"] = ",".join(topic_id_list) if topic_id_list else None
query_params["text"] = text_search query_params["text"] = text_search
threads, result_page, num_pages, text_search_rewrite = Thread.search(query_params) paginated_results = Thread.search(query_params)
# The comments service returns the last page of results if the requested # 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 # page is beyond the last page, but we want be consistent with DRF's general
# behavior and return a PageNotFoundError in that case # behavior and return a PageNotFoundError in that case
if result_page != page: if paginated_results.page != page:
raise PageNotFoundError("Page not found (No results on this page).") raise PageNotFoundError("Page not found (No results on this page).")
results = [ThreadSerializer(thread, context=context).data for thread in threads] results = [ThreadSerializer(thread, context=context).data for thread in paginated_results.collection]
paginator = DiscussionAPIPagination(request, result_page, num_pages) paginator = DiscussionAPIPagination(
request,
paginated_results.page,
paginated_results.num_pages,
paginated_results.thread_count
)
return paginator.get_paginated_response({ return paginator.get_paginated_response({
"results": results, "results": results,
"text_search_rewrite": text_search_rewrite, "text_search_rewrite": paginated_results.corrected_text,
}) })
...@@ -415,7 +420,7 @@ def get_comment_list(request, thread_id, endorsed, page, page_size): ...@@ -415,7 +420,7 @@ def get_comment_list(request, thread_id, endorsed, page, page_size):
num_pages = (resp_total + page_size - 1) / page_size if resp_total else 1 num_pages = (resp_total + page_size - 1) / page_size if resp_total else 1
results = [CommentSerializer(response, context=context).data for response in responses] results = [CommentSerializer(response, context=context).data for response in responses]
paginator = DiscussionAPIPagination(request, page, num_pages) paginator = DiscussionAPIPagination(request, page, num_pages, resp_total)
return paginator.get_paginated_response(results) return paginator.get_paginated_response(results)
...@@ -751,11 +756,14 @@ def get_response_comments(request, comment_id, page, page_size): ...@@ -751,11 +756,14 @@ def get_response_comments(request, comment_id, page, page_size):
response_skip = page_size * (page - 1) response_skip = page_size * (page - 1)
paged_response_comments = response_comments[response_skip:(response_skip + page_size)] paged_response_comments = response_comments[response_skip:(response_skip + page_size)]
if len(paged_response_comments) == 0 and page != 1:
raise PageNotFoundError("Page not found (No results on this page).")
results = [CommentSerializer(comment, context=context).data for comment in paged_response_comments] results = [CommentSerializer(comment, context=context).data for comment in paged_response_comments]
comments_count = len(response_comments) comments_count = len(response_comments)
num_pages = (comments_count + page_size - 1) / page_size if comments_count else 1 num_pages = (comments_count + page_size - 1) / page_size if comments_count else 1
paginator = DiscussionAPIPagination(request, page, num_pages) paginator = DiscussionAPIPagination(request, page, num_pages, comments_count)
return paginator.get_paginated_response(results) return paginator.get_paginated_response(results)
except CommentClientRequestError: except CommentClientRequestError:
raise CommentNotFoundError("Comment not found") raise CommentNotFoundError("Comment not found")
......
...@@ -40,23 +40,22 @@ class DiscussionAPIPagination(NamespacedPageNumberPagination): ...@@ -40,23 +40,22 @@ class DiscussionAPIPagination(NamespacedPageNumberPagination):
Subclasses NamespacedPageNumberPagination to provide custom implementation of pagination metadata Subclasses NamespacedPageNumberPagination to provide custom implementation of pagination metadata
by overriding it's methods by overriding it's methods
""" """
def __init__(self, request, page_num, num_pages): def __init__(self, request, page_num, num_pages, result_count=0):
""" """
Overrides parent constructor to take information from discussion api Overrides parent constructor to take information from discussion api
essential for the parent method essential for the parent method
""" """
self.page = _Page(page_num, num_pages) self.page = _Page(page_num, num_pages)
self.base_url = request.build_absolute_uri() self.base_url = request.build_absolute_uri()
self.count = result_count
super(DiscussionAPIPagination, self).__init__() super(DiscussionAPIPagination, self).__init__()
def get_result_count(self): def get_result_count(self):
""" """
A count can't be calculated with the information coming from the Returns total number of results
Forum's api, so returning 0 for the parent method to work
""" """
# TODO: return actual count return self.count
return 0
def get_num_pages(self): def get_num_pages(self):
""" """
......
...@@ -695,7 +695,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, SharedModuleSto ...@@ -695,7 +695,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, SharedModuleSto
] ]
expected_result = make_paginated_api_response( expected_result = make_paginated_api_response(
results=expected_threads, count=0, num_pages=1, next_link=None, previous_link=None results=expected_threads, count=2, num_pages=1, next_link=None, previous_link=None
) )
expected_result.update({"text_search_rewrite": None}) expected_result.update({"text_search_rewrite": None})
self.assertEqual( self.assertEqual(
......
...@@ -308,18 +308,18 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): ...@@ -308,18 +308,18 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
}] }]
self.register_get_threads_response(source_threads, page=1, num_pages=2) self.register_get_threads_response(source_threads, page=1, num_pages=2)
response = self.client.get(self.url, {"course_id": unicode(self.course.id), "following": ""}) response = self.client.get(self.url, {"course_id": unicode(self.course.id), "following": ""})
expected_respoonse = make_paginated_api_response( expected_response = make_paginated_api_response(
results=expected_threads, results=expected_threads,
count=0, count=1,
num_pages=2, num_pages=2,
next_link="http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz&page=2", next_link="http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz&page=2",
previous_link=None previous_link=None
) )
expected_respoonse.update({"text_search_rewrite": None}) expected_response.update({"text_search_rewrite": None})
self.assert_response_correct( self.assert_response_correct(
response, response,
200, 200,
expected_respoonse expected_response
) )
self.assert_last_query_params({ self.assert_last_query_params({
"user_id": [unicode(self.user.id)], "user_id": [unicode(self.user.id)],
...@@ -954,7 +954,7 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): ...@@ -954,7 +954,7 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
response, response,
200, 200,
make_paginated_api_response( make_paginated_api_response(
results=expected_comments, count=0, num_pages=10, next_link=next_link, previous_link=None results=expected_comments, count=100, num_pages=10, next_link=next_link, previous_link=None
) )
) )
self.assert_query_params_equal( self.assert_query_params_equal(
...@@ -1440,3 +1440,29 @@ class CommentViewSetRetrieveTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase ...@@ -1440,3 +1440,29 @@ class CommentViewSetRetrieveTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase
self.register_get_comment_error_response(self.comment_id, 404) self.register_get_comment_error_response(self.comment_id, 404)
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_pagination(self):
"""
Test that pagination parameters are correctly plumbed through to the
comments service and that a 404 is correctly returned if a page past the
end is requested
"""
self.register_get_user_response(self.user)
cs_comment_child = self.make_comment_data("test_child_comment", self.comment_id, children=[])
cs_comment = self.make_comment_data(self.comment_id, None, [cs_comment_child])
cs_thread = make_minimal_cs_thread({
"id": self.thread_id,
"course_id": unicode(self.course.id),
"children": [cs_comment],
})
self.register_get_thread_response(cs_thread)
self.register_get_comment_response(cs_comment)
response = self.client.get(
self.url,
{"comment_id": self.comment_id, "page": "18", "page_size": "4"}
)
self.assert_response_correct(
response,
404,
{"developer_message": "Page not found (No results on this page)."}
)
...@@ -67,6 +67,7 @@ class CommentsServiceMockMixin(object): ...@@ -67,6 +67,7 @@ class CommentsServiceMockMixin(object):
"collection": threads, "collection": threads,
"page": page, "page": page,
"num_pages": num_pages, "num_pages": num_pages,
"thread_count": len(threads),
}), }),
status=200 status=200
) )
...@@ -81,6 +82,7 @@ class CommentsServiceMockMixin(object): ...@@ -81,6 +82,7 @@ class CommentsServiceMockMixin(object):
"page": 1, "page": 1,
"num_pages": num_pages, "num_pages": num_pages,
"corrected_text": rewrite, "corrected_text": rewrite,
"thread_count": len(threads),
}), }),
status=200 status=200
) )
...@@ -200,6 +202,7 @@ class CommentsServiceMockMixin(object): ...@@ -200,6 +202,7 @@ class CommentsServiceMockMixin(object):
"collection": threads, "collection": threads,
"page": page, "page": page,
"num_pages": num_pages, "num_pages": num_pages,
"thread_count": len(threads),
}), }),
status=200 status=200
) )
......
...@@ -6,6 +6,7 @@ from django.core.urlresolvers import reverse ...@@ -6,6 +6,7 @@ from django.core.urlresolvers import reverse
from django.http import Http404 from django.http import Http404
from django.test.client import Client, RequestFactory from django.test.client import Client, RequestFactory
from django.test.utils import override_settings from django.test.utils import override_settings
from lms.lib.comment_client.utils import CommentClientPaginatedResult
from edxmako.tests import mako_middleware_process_request from edxmako.tests import mako_middleware_process_request
from django_comment_common.utils import ThreadContext from django_comment_common.utils import ThreadContext
...@@ -96,7 +97,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase): ...@@ -96,7 +97,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase):
# Mock the code that makes the HTTP requests to the cs_comment_service app # Mock the code that makes the HTTP requests to the cs_comment_service app
# for the profiled user's active threads # for the profiled user's active threads
mock_threads.return_value = [], 1, 1 mock_threads.return_value = CommentClientPaginatedResult(collection=[], page=1, num_pages=1)
# Mock the code that makes the HTTP request to the cs_comment_service app # Mock the code that makes the HTTP request to the cs_comment_service app
# that gets the current user's info # that gets the current user's info
......
...@@ -146,7 +146,8 @@ def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE): ...@@ -146,7 +146,8 @@ def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE):
) )
) )
threads, page, num_pages, corrected_text = cc.Thread.search(query_params) paginated_results = cc.Thread.search(query_params)
threads = paginated_results.collection
# If not provided with a discussion id, filter threads by commentable ids # If not provided with a discussion id, filter threads by commentable ids
# which are accessible to the current user. # which are accessible to the current user.
...@@ -162,9 +163,9 @@ def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE): ...@@ -162,9 +163,9 @@ def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE):
if 'pinned' not in thread: if 'pinned' not in thread:
thread['pinned'] = False thread['pinned'] = False
query_params['page'] = page query_params['page'] = paginated_results.page
query_params['num_pages'] = num_pages query_params['num_pages'] = paginated_results.num_pages
query_params['corrected_text'] = corrected_text query_params['corrected_text'] = paginated_results.corrected_text
return threads, query_params return threads, query_params
...@@ -336,7 +337,12 @@ def single_thread(request, course_key, discussion_id, thread_id): ...@@ -336,7 +337,12 @@ def single_thread(request, course_key, discussion_id, thread_id):
is_staff = has_permission(request.user, 'openclose_thread', course.id) is_staff = has_permission(request.user, 'openclose_thread', course.id)
if request.is_ajax(): if request.is_ajax():
with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"): with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"):
annotated_content_info = utils.get_annotated_content_infos(course_key, thread, request.user, user_info=user_info) annotated_content_info = utils.get_annotated_content_infos(
course_key,
thread,
request.user,
user_info=user_info
)
content = utils.prepare_content(thread.to_dict(), course_key, is_staff) content = utils.prepare_content(thread.to_dict(), course_key, is_staff)
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
add_courseware_context([content], course, request.user) add_courseware_context([content], course, request.user)
...@@ -511,18 +517,26 @@ def followed_threads(request, course_key, user_id): ...@@ -511,18 +517,26 @@ def followed_threads(request, course_key, user_id):
if group_id is not None: if group_id is not None:
query_params['group_id'] = group_id query_params['group_id'] = group_id
threads, page, num_pages = profiled_user.subscribed_threads(query_params) paginated_results = profiled_user.subscribed_threads(query_params)
query_params['page'] = page print "\n \n \n paginated results \n \n \n "
query_params['num_pages'] = num_pages print paginated_results
query_params['page'] = paginated_results.page
query_params['num_pages'] = paginated_results.num_pages
user_info = cc.User.from_django_user(request.user).to_dict() user_info = cc.User.from_django_user(request.user).to_dict()
with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"):
annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) annotated_content_info = utils.get_metadata_for_threads(
course_key,
paginated_results.collection,
request.user, user_info
)
if request.is_ajax(): if request.is_ajax():
is_staff = has_permission(request.user, 'openclose_thread', course.id) is_staff = has_permission(request.user, 'openclose_thread', course.id)
return utils.JsonResponse({ return utils.JsonResponse({
'annotated_content_info': annotated_content_info, 'annotated_content_info': annotated_content_info,
'discussion_data': [utils.prepare_content(thread, course_key, is_staff) for thread in threads], 'discussion_data': [
utils.prepare_content(thread, course_key, is_staff) for thread in paginated_results.collection
],
'page': query_params['page'], 'page': query_params['page'],
'num_pages': query_params['num_pages'], 'num_pages': query_params['num_pages'],
}) })
...@@ -533,7 +547,7 @@ def followed_threads(request, course_key, user_id): ...@@ -533,7 +547,7 @@ def followed_threads(request, course_key, user_id):
'user': request.user, 'user': request.user,
'django_user': User.objects.get(id=user_id), 'django_user': User.objects.get(id=user_id),
'profiled_user': profiled_user.to_dict(), 'profiled_user': profiled_user.to_dict(),
'threads': json.dumps(threads), 'threads': json.dumps(paginated_results.collection),
'user_info': json.dumps(user_info), 'user_info': json.dumps(user_info),
'annotated_content_info': json.dumps(annotated_content_info), 'annotated_content_info': json.dumps(annotated_content_info),
# 'content': content, # 'content': content,
......
import logging import logging
from eventtracking import tracker from eventtracking import tracker
from .utils import merge_dict, strip_blank, strip_none, extract, perform_request from .utils import merge_dict, strip_blank, strip_none, extract, perform_request, CommentClientPaginatedResult
from .utils import CommentClientRequestError from .utils import CommentClientRequestError
import models import models
import settings import settings
...@@ -94,7 +94,14 @@ class Thread(models.Model): ...@@ -94,7 +94,14 @@ class Thread(models.Model):
total_results=total_results total_results=total_results
) )
) )
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1), response.get('corrected_text')
return CommentClientPaginatedResult(
collection=response.get('collection', []),
page=response.get('page', 1),
num_pages=response.get('num_pages', 1),
thread_count=response.get('thread_count', 0),
corrected_text=response.get('corrected_text', None)
)
@classmethod @classmethod
def url_for_threads(cls, params={}): def url_for_threads(cls, params={}):
......
from .utils import merge_dict, perform_request, CommentClientRequestError """ User model wrapper for comment service"""
from .utils import merge_dict, perform_request, CommentClientRequestError, CommentClientPaginatedResult
import models import models
import settings import settings
...@@ -113,7 +114,12 @@ class User(models.Model): ...@@ -113,7 +114,12 @@ class User(models.Model):
metric_tags=self._metric_tags, metric_tags=self._metric_tags,
paged_results=True paged_results=True
) )
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1) return CommentClientPaginatedResult(
collection=response.get('collection', []),
page=response.get('page', 1),
num_pages=response.get('num_pages', 1),
thread_count=response.get('thread_count', 0)
)
def _retrieve(self, *args, **kwargs): def _retrieve(self, *args, **kwargs):
url = self.url(action='get', params=self.attributes) url = self.url(action='get', params=self.attributes)
......
"""" Common utilities for comment client wrapper """
from contextlib import contextmanager from contextlib import contextmanager
import dogstats_wrapper as dog_stats_api import dogstats_wrapper as dog_stats_api
import logging import logging
...@@ -141,9 +142,9 @@ class CommentClientError(Exception): ...@@ -141,9 +142,9 @@ class CommentClientError(Exception):
class CommentClientRequestError(CommentClientError): class CommentClientRequestError(CommentClientError):
def __init__(self, msg, status_code=400): def __init__(self, msg, status_codes=400):
super(CommentClientRequestError, self).__init__(msg) super(CommentClientRequestError, self).__init__(msg)
self.status_code = status_code self.status_code = status_codes
class CommentClient500Error(CommentClientError): class CommentClient500Error(CommentClientError):
...@@ -152,3 +153,14 @@ class CommentClient500Error(CommentClientError): ...@@ -152,3 +153,14 @@ class CommentClient500Error(CommentClientError):
class CommentClientMaintenanceError(CommentClientError): class CommentClientMaintenanceError(CommentClientError):
pass pass
class CommentClientPaginatedResult(object):
""" class for paginated results returned from comment services"""
def __init__(self, collection, page, num_pages, thread_count=0, corrected_text=None):
self.collection = collection
self.page = page
self.num_pages = num_pages
self.thread_count = thread_count
self.corrected_text = corrected_text
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment