Commit ce3c650c by jsa

use modulestore bulk ops in forum views.

JIRA: PLAT-348
parent de22ccd5
import json import json
import logging import logging
import ddt
from django.core.urlresolvers import reverse 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
...@@ -17,8 +18,14 @@ from django_comment_client.tests.utils import CohortedContentTestCase ...@@ -17,8 +18,14 @@ from django_comment_client.tests.utils import CohortedContentTestCase
from django_comment_client.utils import strip_none from django_comment_client.utils import strip_none
from student.tests.factories import UserFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, TEST_DATA_MOCK_MODULESTORE from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase,
TEST_DATA_MOCK_MODULESTORE,
TEST_DATA_MONGO_MODULESTORE
)
from xmodule.modulestore.tests.factories import check_mongo_calls, CourseFactory, ItemFactory
from courseware.courses import UserNotEnrolled from courseware.courses import UserNotEnrolled
from nose.tools import assert_true # pylint: disable=E0611 from nose.tools import assert_true # pylint: disable=E0611
...@@ -98,7 +105,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase): ...@@ -98,7 +105,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase):
self.assertEqual(self.response.status_code, 404) self.assertEqual(self.response.status_code, 404)
def make_mock_thread_data(text, thread_id, include_children, group_id=None, group_name=None, commentable_id=None): def make_mock_thread_data(text, thread_id, num_children, group_id=None, group_name=None, commentable_id=None):
thread_data = { thread_data = {
"id": thread_id, "id": thread_id,
"type": "thread", "type": "thread",
...@@ -112,25 +119,31 @@ def make_mock_thread_data(text, thread_id, include_children, group_id=None, grou ...@@ -112,25 +119,31 @@ def make_mock_thread_data(text, thread_id, include_children, group_id=None, grou
} }
if group_id is not None: if group_id is not None:
thread_data['group_name'] = group_name thread_data['group_name'] = group_name
if include_children: if num_children is not None:
thread_data["children"] = [{ thread_data["children"] = [{
"id": "dummy_comment_id", "id": "dummy_comment_id_{}".format(i),
"type": "comment", "type": "comment",
"body": text, "body": text,
}] } for i in range(num_children)]
return thread_data return thread_data
def make_mock_request_impl(text, thread_id="dummy_thread_id", group_id=None, commentable_id=None): def make_mock_request_impl(
text,
thread_id="dummy_thread_id",
group_id=None,
commentable_id=None,
num_thread_responses=1,
):
def mock_request_impl(*args, **kwargs): def mock_request_impl(*args, **kwargs):
url = args[1] url = args[1]
data = None data = None
if url.endswith("threads") or url.endswith("user_profile"): if url.endswith("threads") or url.endswith("user_profile"):
data = { data = {
"collection": [make_mock_thread_data(text, thread_id, False, group_id=group_id, commentable_id=commentable_id)] "collection": [make_mock_thread_data(text, thread_id, None, group_id=group_id, commentable_id=commentable_id)]
} }
elif thread_id and url.endswith(thread_id): elif thread_id and url.endswith(thread_id):
data = make_mock_thread_data(text, thread_id, True, group_id=group_id) data = make_mock_thread_data(text, thread_id, num_thread_responses, group_id=group_id)
elif "/users/" in url: elif "/users/" in url:
data = { data = {
"default_sort_key": "date", "default_sort_key": "date",
...@@ -200,7 +213,7 @@ class SingleThreadTestCase(ModuleStoreTestCase): ...@@ -200,7 +213,7 @@ class SingleThreadTestCase(ModuleStoreTestCase):
# django view performs prior to writing thread data to the response # django view performs prior to writing thread data to the response
self.assertEquals( self.assertEquals(
response_data["content"], response_data["content"],
strip_none(make_mock_thread_data(text, thread_id, True)) strip_none(make_mock_thread_data(text, thread_id, 1))
) )
mock_request.assert_called_with( mock_request.assert_called_with(
"get", "get",
...@@ -236,7 +249,7 @@ class SingleThreadTestCase(ModuleStoreTestCase): ...@@ -236,7 +249,7 @@ class SingleThreadTestCase(ModuleStoreTestCase):
# django view performs prior to writing thread data to the response # django view performs prior to writing thread data to the response
self.assertEquals( self.assertEquals(
response_data["content"], response_data["content"],
strip_none(make_mock_thread_data(text, thread_id, True)) strip_none(make_mock_thread_data(text, thread_id, 1))
) )
mock_request.assert_called_with( mock_request.assert_called_with(
"get", "get",
...@@ -278,6 +291,54 @@ class SingleThreadTestCase(ModuleStoreTestCase): ...@@ -278,6 +291,54 @@ class SingleThreadTestCase(ModuleStoreTestCase):
) )
@ddt.ddt
@patch('requests.request')
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class SingleThreadQueryCountTestCase(ModuleStoreTestCase):
"""
Ensures the number of modulestore queries is deterministic based on the
number of responses retrieved for a given discussion thread.
"""
@ddt.data(
# old mongo: number of responses plus 16. TODO: O(n)!
(ModuleStoreEnum.Type.mongo, 1, 17),
(ModuleStoreEnum.Type.mongo, 50, 66),
# split mongo: 3 queries, regardless of thread response size.
(ModuleStoreEnum.Type.split, 1, 3),
(ModuleStoreEnum.Type.split, 50, 3),
)
@ddt.unpack
def test_number_of_mongo_queries(self, default_store, num_thread_responses, num_mongo_calls, mock_request):
with modulestore().default_store(default_store):
course = CourseFactory.create()
student = UserFactory.create()
CourseEnrollmentFactory.create(user=student, course_id=course.id)
test_thread_id = "test_thread_id"
mock_request.side_effect = make_mock_request_impl(
"dummy content",
test_thread_id,
num_thread_responses=num_thread_responses,
)
request = RequestFactory().get(
"dummy_url",
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
)
request.user = student
with check_mongo_calls(num_mongo_calls):
response = views.single_thread(
request,
course.id.to_deprecated_string(),
"dummy_discussion_id",
test_thread_id
)
self.assertEquals(response.status_code, 200)
self.assertEquals(len(json.loads(response.content)["content"]["children"]), num_thread_responses)
@override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE)
@patch('requests.request') @patch('requests.request')
class SingleCohortedThreadTestCase(CohortedContentTestCase): class SingleCohortedThreadTestCase(CohortedContentTestCase):
...@@ -309,7 +370,7 @@ class SingleCohortedThreadTestCase(CohortedContentTestCase): ...@@ -309,7 +370,7 @@ class SingleCohortedThreadTestCase(CohortedContentTestCase):
self.assertEquals( self.assertEquals(
response_data["content"], response_data["content"],
make_mock_thread_data( make_mock_thread_data(
self.mock_text, self.mock_thread_id, True, self.mock_text, self.mock_thread_id, 1,
group_id=self.student_cohort.id, group_id=self.student_cohort.id,
group_name=self.student_cohort.name, group_name=self.student_cohort.name,
) )
......
"""
Views handling read (GET) requests for the Discussion tab and inline discussions.
"""
from functools import wraps
import json import json
import logging import logging
import xml.sax.saxutils as saxutils import xml.sax.saxutils as saxutils
...@@ -18,6 +23,7 @@ from openedx.core.djangoapps.course_groups.cohorts import ( ...@@ -18,6 +23,7 @@ from openedx.core.djangoapps.course_groups.cohorts import (
is_commentable_cohorted is_commentable_cohorted
) )
from courseware.access import has_access from courseware.access import has_access
from xmodule.modulestore.django import modulestore
from django_comment_client.permissions import cached_has_permission from django_comment_client.permissions import cached_has_permission
from django_comment_client.utils import ( from django_comment_client.utils import (
...@@ -30,7 +36,7 @@ from django_comment_client.utils import ( ...@@ -30,7 +36,7 @@ from django_comment_client.utils import (
import django_comment_client.utils as utils import django_comment_client.utils as utils
import lms.lib.comment_client as cc import lms.lib.comment_client as cc
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.keys import CourseKey
THREADS_PER_PAGE = 20 THREADS_PER_PAGE = 20
INLINE_THREADS_PER_PAGE = 20 INLINE_THREADS_PER_PAGE = 20
...@@ -130,13 +136,27 @@ def get_threads(request, course_key, discussion_id=None, per_page=THREADS_PER_PA ...@@ -130,13 +136,27 @@ def get_threads(request, course_key, discussion_id=None, per_page=THREADS_PER_PA
return threads, query_params return threads, query_params
def use_bulk_ops(view_func):
"""
Wraps internal request handling inside a modulestore bulk op, significantly
reducing redundant database calls. Also converts the course_id parsed from
the request uri to a CourseKey before passing to the view.
"""
@wraps(view_func)
def wrapped_view(request, course_id, *args, **kwargs): # pylint: disable=missing-docstring
course_key = CourseKey.from_string(course_id)
with modulestore().bulk_operations(course_key):
return view_func(request, course_key, *args, **kwargs)
return wrapped_view
@login_required @login_required
def inline_discussion(request, course_id, discussion_id): @use_bulk_ops
def inline_discussion(request, course_key, discussion_id):
""" """
Renders JSON for DiscussionModules Renders JSON for DiscussionModules
""" """
nr_transaction = newrelic.agent.current_transaction() nr_transaction = newrelic.agent.current_transaction()
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course = get_course_with_access(request.user, 'load_forum', course_key) course = get_course_with_access(request.user, 'load_forum', course_key)
cc_user = cc.User.from_django_user(request.user) cc_user = cc.User.from_django_user(request.user)
...@@ -165,11 +185,11 @@ def inline_discussion(request, course_id, discussion_id): ...@@ -165,11 +185,11 @@ def inline_discussion(request, course_id, discussion_id):
@login_required @login_required
def forum_form_discussion(request, course_id): @use_bulk_ops
def forum_form_discussion(request, course_key):
""" """
Renders the main Discussion page, potentially filtered by a search query Renders the main Discussion page, potentially filtered by a search query
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
nr_transaction = newrelic.agent.current_transaction() nr_transaction = newrelic.agent.current_transaction()
course = get_course_with_access(request.user, 'load_forum', course_key, check_if_enrolled=True) course = get_course_with_access(request.user, 'load_forum', course_key, check_if_enrolled=True)
...@@ -232,8 +252,12 @@ def forum_form_discussion(request, course_id): ...@@ -232,8 +252,12 @@ def forum_form_discussion(request, course_id):
@require_GET @require_GET
@login_required @login_required
def single_thread(request, course_id, discussion_id, thread_id): @use_bulk_ops
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) def single_thread(request, course_key, discussion_id, thread_id):
"""
Renders a response to display a single discussion thread.
"""
nr_transaction = newrelic.agent.current_transaction() nr_transaction = newrelic.agent.current_transaction()
course = get_course_with_access(request.user, 'load_forum', course_key) course = get_course_with_access(request.user, 'load_forum', course_key)
...@@ -325,8 +349,13 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -325,8 +349,13 @@ def single_thread(request, course_id, discussion_id, thread_id):
@require_GET @require_GET
@login_required @login_required
def user_profile(request, course_id, user_id): @use_bulk_ops
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) def user_profile(request, course_key, user_id):
"""
Renders a response to display the user profile page (shown after clicking
on a post author's username).
"""
nr_transaction = newrelic.agent.current_transaction() nr_transaction = newrelic.agent.current_transaction()
#TODO: Allow sorting? #TODO: Allow sorting?
...@@ -383,8 +412,12 @@ def user_profile(request, course_id, user_id): ...@@ -383,8 +412,12 @@ def user_profile(request, course_id, user_id):
@login_required @login_required
def followed_threads(request, course_id, user_id): @use_bulk_ops
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) def followed_threads(request, course_key, user_id):
"""
Ajax-only endpoint retrieving the threads followed by a specific user.
"""
nr_transaction = newrelic.agent.current_transaction() nr_transaction = newrelic.agent.current_transaction()
course = get_course_with_access(request.user, 'load_forum', course_key) course = get_course_with_access(request.user, 'load_forum', course_key)
......
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