Commit c8434ef9 by Daniel Friedman

Check access for discussion modules in forums

TNL-650
parent 70455d3e
......@@ -311,13 +311,15 @@ class DiscussionSortPreferencePage(CoursePage):
class DiscussionTabSingleThreadPage(CoursePage):
def __init__(self, browser, course_id, thread_id):
def __init__(self, browser, course_id, discussion_id, thread_id):
super(DiscussionTabSingleThreadPage, self).__init__(browser, course_id)
self.thread_page = DiscussionThreadPage(
browser,
"body.discussion .discussion-article[data-id='{thread_id}']".format(thread_id=thread_id)
)
self.url_path = "discussion/forum/dummy/threads/" + thread_id
self.url_path = "discussion/forum/{discussion_id}/threads/{thread_id}".format(
discussion_id=discussion_id, thread_id=thread_id
)
def is_browser_on_page(self):
return self.thread_page.is_browser_on_page()
......
......@@ -5,12 +5,15 @@ Helper functions and classes for discussion tests.
from uuid import uuid4
import json
from ...fixtures import LMS_BASE_URL
from ...fixtures.course import CourseFixture
from ...fixtures.discussion import (
SingleThreadViewFixture,
Thread,
Response,
)
from ...fixtures import LMS_BASE_URL
from ...pages.lms.discussion import DiscussionTabSingleThreadPage
from ...tests.helpers import UniqueCourseTest
class BaseDiscussionMixin(object):
......@@ -83,3 +86,22 @@ class CohortTestMixin(object):
data = {"users": username}
response = course_fixture.session.post(url, data=data, headers=course_fixture.headers)
self.assertTrue(response.ok, "Failed to add user to cohort")
class BaseDiscussionTestCase(UniqueCourseTest):
def setUp(self):
super(BaseDiscussionTestCase, self).setUp()
self.discussion_id = "test_discussion_{}".format(uuid4().hex)
self.course_fixture = CourseFixture(**self.course_info)
self.course_fixture.add_advanced_settings(
{'discussion_topics': {'value': {'Test Discussion Topic': {'id': self.discussion_id}}}}
)
self.course_fixture.install()
def create_single_thread_page(self, thread_id):
"""
Sets up a `DiscussionTabSingleThreadPage` for a given
`thread_id`.
"""
return DiscussionTabSingleThreadPage(self.browser, self.course_id, self.discussion_id, thread_id)
......@@ -3,7 +3,7 @@ Tests related to the cohorting feature.
"""
from uuid import uuid4
from .helpers import BaseDiscussionMixin
from .helpers import BaseDiscussionMixin, BaseDiscussionTestCase
from .helpers import CohortTestMixin
from ..helpers import UniqueCourseTest
from ...pages.lms.auto_auth import AutoAuthPage
......@@ -57,20 +57,17 @@ class CohortedDiscussionTestMixin(BaseDiscussionMixin, CohortTestMixin):
self.assertEquals(self.thread_page.get_group_visibility_label(), "This post is visible to everyone.")
class DiscussionTabSingleThreadTest(UniqueCourseTest):
class DiscussionTabSingleThreadTest(BaseDiscussionTestCase):
"""
Tests for the discussion page displaying a single thread.
"""
def setUp(self):
super(DiscussionTabSingleThreadTest, self).setUp()
self.discussion_id = "test_discussion_{}".format(uuid4().hex)
# Create a course to register for
self.course_fixture = CourseFixture(**self.course_info).install()
self.setup_cohorts()
AutoAuthPage(self.browser, course_id=self.course_id).visit()
def setup_thread_page(self, thread_id):
self.thread_page = DiscussionTabSingleThreadPage(self.browser, self.course_id, thread_id) # pylint: disable=attribute-defined-outside-init
self.thread_page = DiscussionTabSingleThreadPage(self.browser, self.course_id, self.discussion_id, thread_id) # pylint: disable=attribute-defined-outside-init
self.thread_page.visit()
# pylint: disable=unused-argument
......
......@@ -7,6 +7,7 @@ from pytz import UTC
from uuid import uuid4
from nose.plugins.attrib import attr
from .helpers import BaseDiscussionTestCase
from ..helpers import UniqueCourseTest
from ...pages.lms.auto_auth import AutoAuthPage
from ...pages.lms.courseware import CoursewarePage
......@@ -139,22 +140,17 @@ class DiscussionHomePageTest(UniqueCourseTest):
@attr('shard_1')
class DiscussionTabSingleThreadTest(UniqueCourseTest, DiscussionResponsePaginationTestMixin):
class DiscussionTabSingleThreadTest(BaseDiscussionTestCase, DiscussionResponsePaginationTestMixin):
"""
Tests for the discussion page displaying a single thread
"""
def setUp(self):
super(DiscussionTabSingleThreadTest, self).setUp()
self.discussion_id = "test_discussion_{}".format(uuid4().hex)
# Create a course to register for
CourseFixture(**self.course_info).install()
AutoAuthPage(self.browser, course_id=self.course_id).visit()
def setup_thread_page(self, thread_id):
self.thread_page = DiscussionTabSingleThreadPage(self.browser, self.course_id, thread_id) # pylint: disable=attribute-defined-outside-init
self.thread_page = self.create_single_thread_page(thread_id) # pylint: disable=attribute-defined-outside-init
self.thread_page.visit()
def test_marked_answer_comments(self):
......@@ -180,7 +176,7 @@ class DiscussionTabSingleThreadTest(UniqueCourseTest, DiscussionResponsePaginati
@attr('shard_1')
class DiscussionOpenClosedThreadTest(UniqueCourseTest):
class DiscussionOpenClosedThreadTest(BaseDiscussionTestCase):
"""
Tests for checking the display of attributes on open and closed threads
"""
......@@ -188,8 +184,6 @@ class DiscussionOpenClosedThreadTest(UniqueCourseTest):
def setUp(self):
super(DiscussionOpenClosedThreadTest, self).setUp()
# Create a course to register for
CourseFixture(**self.course_info).install()
self.thread_id = "test_thread_{}".format(uuid4().hex)
def setup_user(self, roles=[]):
......@@ -197,6 +191,7 @@ class DiscussionOpenClosedThreadTest(UniqueCourseTest):
self.user_id = AutoAuthPage(self.browser, course_id=self.course_id, roles=roles_str).visit().get_user_id()
def setup_view(self, **thread_kwargs):
thread_kwargs.update({'commentable_id': self.discussion_id})
view = SingleThreadViewFixture(
Thread(id=self.thread_id, **thread_kwargs)
)
......@@ -209,7 +204,7 @@ class DiscussionOpenClosedThreadTest(UniqueCourseTest):
self.setup_view(closed=True)
else:
self.setup_view()
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, self.thread_id)
page = self.create_single_thread_page(self.thread_id)
page.visit()
page.close_open_thread()
return page
......@@ -230,23 +225,16 @@ class DiscussionOpenClosedThreadTest(UniqueCourseTest):
@attr('shard_1')
class DiscussionCommentDeletionTest(UniqueCourseTest):
class DiscussionCommentDeletionTest(BaseDiscussionTestCase):
"""
Tests for deleting comments displayed beneath responses in the single thread view.
"""
def setUp(self):
super(DiscussionCommentDeletionTest, self).setUp()
# Create a course to register for
CourseFixture(**self.course_info).install()
def setup_user(self, roles=[]):
roles_str = ','.join(roles)
self.user_id = AutoAuthPage(self.browser, course_id=self.course_id, roles=roles_str).visit().get_user_id()
def setup_view(self):
view = SingleThreadViewFixture(Thread(id="comment_deletion_test_thread"))
view = SingleThreadViewFixture(Thread(id="comment_deletion_test_thread", commentable_id=self.discussion_id))
view.addResponse(
Response(id="response1"),
[Comment(id="comment_other_author", user_id="other"), Comment(id="comment_self_author", user_id=self.user_id)])
......@@ -255,7 +243,7 @@ class DiscussionCommentDeletionTest(UniqueCourseTest):
def test_comment_deletion_as_student(self):
self.setup_user()
self.setup_view()
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, "comment_deletion_test_thread")
page = self.create_single_thread_page("comment_deletion_test_thread")
page.visit()
self.assertTrue(page.is_comment_deletable("comment_self_author"))
self.assertTrue(page.is_comment_visible("comment_other_author"))
......@@ -265,7 +253,7 @@ class DiscussionCommentDeletionTest(UniqueCourseTest):
def test_comment_deletion_as_moderator(self):
self.setup_user(roles=['Moderator'])
self.setup_view()
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, "comment_deletion_test_thread")
page = self.create_single_thread_page("comment_deletion_test_thread")
page.visit()
self.assertTrue(page.is_comment_deletable("comment_self_author"))
self.assertTrue(page.is_comment_deletable("comment_other_author"))
......@@ -274,23 +262,16 @@ class DiscussionCommentDeletionTest(UniqueCourseTest):
@attr('shard_1')
class DiscussionResponseEditTest(UniqueCourseTest):
class DiscussionResponseEditTest(BaseDiscussionTestCase):
"""
Tests for editing responses displayed beneath thread in the single thread view.
"""
def setUp(self):
super(DiscussionResponseEditTest, self).setUp()
# Create a course to register for
CourseFixture(**self.course_info).install()
def setup_user(self, roles=[]):
roles_str = ','.join(roles)
self.user_id = AutoAuthPage(self.browser, course_id=self.course_id, roles=roles_str).visit().get_user_id()
def setup_view(self):
view = SingleThreadViewFixture(Thread(id="response_edit_test_thread"))
view = SingleThreadViewFixture(Thread(id="response_edit_test_thread", commentable_id=self.discussion_id))
view.addResponse(
Response(id="response_other_author", user_id="other", thread_id="response_edit_test_thread"),
)
......@@ -317,7 +298,7 @@ class DiscussionResponseEditTest(UniqueCourseTest):
"""
self.setup_user()
self.setup_view()
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, "response_edit_test_thread")
page = self.create_single_thread_page("response_edit_test_thread")
page.visit()
self.assertTrue(page.is_response_visible("response_other_author"))
self.assertFalse(page.is_response_editable("response_other_author"))
......@@ -334,7 +315,7 @@ class DiscussionResponseEditTest(UniqueCourseTest):
"""
self.setup_user(roles=["Moderator"])
self.setup_view()
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, "response_edit_test_thread")
page = self.create_single_thread_page("response_edit_test_thread")
page.visit()
self.edit_response(page, "response_self_author")
self.edit_response(page, "response_other_author")
......@@ -362,7 +343,7 @@ class DiscussionResponseEditTest(UniqueCourseTest):
"""
self.setup_user(roles=["Moderator"])
self.setup_view()
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, "response_edit_test_thread")
page = self.create_single_thread_page("response_edit_test_thread")
page.visit()
self.edit_response(page, "response_self_author")
self.edit_response(page, "response_other_author")
......@@ -375,23 +356,16 @@ class DiscussionResponseEditTest(UniqueCourseTest):
@attr('shard_1')
class DiscussionCommentEditTest(UniqueCourseTest):
class DiscussionCommentEditTest(BaseDiscussionTestCase):
"""
Tests for editing comments displayed beneath responses in the single thread view.
"""
def setUp(self):
super(DiscussionCommentEditTest, self).setUp()
# Create a course to register for
CourseFixture(**self.course_info).install()
def setup_user(self, roles=[]):
roles_str = ','.join(roles)
self.user_id = AutoAuthPage(self.browser, course_id=self.course_id, roles=roles_str).visit().get_user_id()
def setup_view(self):
view = SingleThreadViewFixture(Thread(id="comment_edit_test_thread"))
view = SingleThreadViewFixture(Thread(id="comment_edit_test_thread", commentable_id=self.discussion_id))
view.addResponse(
Response(id="response1"),
[Comment(id="comment_other_author", user_id="other"), Comment(id="comment_self_author", user_id=self.user_id)])
......@@ -406,7 +380,7 @@ class DiscussionCommentEditTest(UniqueCourseTest):
def test_edit_comment_as_student(self):
self.setup_user()
self.setup_view()
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page = self.create_single_thread_page("comment_edit_test_thread")
page.visit()
self.assertTrue(page.is_comment_editable("comment_self_author"))
self.assertTrue(page.is_comment_visible("comment_other_author"))
......@@ -416,7 +390,7 @@ class DiscussionCommentEditTest(UniqueCourseTest):
def test_edit_comment_as_moderator(self):
self.setup_user(roles=["Moderator"])
self.setup_view()
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page = self.create_single_thread_page("comment_edit_test_thread")
page.visit()
self.assertTrue(page.is_comment_editable("comment_self_author"))
self.assertTrue(page.is_comment_editable("comment_other_author"))
......@@ -426,7 +400,7 @@ class DiscussionCommentEditTest(UniqueCourseTest):
def test_cancel_comment_edit(self):
self.setup_user()
self.setup_view()
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page = self.create_single_thread_page("comment_edit_test_thread")
page.visit()
self.assertTrue(page.is_comment_editable("comment_self_author"))
original_body = page.get_comment_body("comment_self_author")
......@@ -438,7 +412,7 @@ class DiscussionCommentEditTest(UniqueCourseTest):
"""Only one editor should be visible at a time within a single response"""
self.setup_user(roles=["Moderator"])
self.setup_view()
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page = self.create_single_thread_page("comment_edit_test_thread")
page.visit()
self.assertTrue(page.is_comment_editable("comment_self_author"))
self.assertTrue(page.is_comment_editable("comment_other_author"))
......
......@@ -13,7 +13,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE
from django_comment_client.base import views
from django_comment_client.tests.group_id import CohortedTopicGroupIdTestMixin, NonCohortedTopicGroupIdTestMixin, GroupIdAssertionMixin
from django_comment_client.tests.utils import CohortedContentTestCase
from django_comment_client.tests.utils import CohortedTestCase
from django_comment_client.tests.unicode import UnicodeTestMixin
from django_comment_common.models import Role
from django_comment_common.utils import seed_permissions_roles
......@@ -41,7 +41,7 @@ class MockRequestSetupMixin(object):
@patch('lms.lib.comment_client.utils.requests.request')
class CreateThreadGroupIdTestCase(
MockRequestSetupMixin,
CohortedContentTestCase,
CohortedTestCase,
CohortedTopicGroupIdTestMixin,
NonCohortedTopicGroupIdTestMixin
):
......@@ -76,7 +76,7 @@ class CreateThreadGroupIdTestCase(
@patch('lms.lib.comment_client.utils.requests.request')
class ThreadActionGroupIdTestCase(
MockRequestSetupMixin,
CohortedContentTestCase,
CohortedTestCase,
GroupIdAssertionMixin
):
def call_view(
......
......@@ -120,7 +120,7 @@ def create_thread(request, course_id, commentable_id):
user = cc.User.from_django_user(request.user)
user.follow(thread)
data = thread.to_dict()
add_courseware_context([data], course)
add_courseware_context([data], course, request.user)
if request.is_ajax():
return ajax_content_response(request, course_key, data)
else:
......@@ -149,7 +149,7 @@ def update_thread(request, course_id, thread_id):
thread.thread_type = request.POST["thread_type"]
if "commentable_id" in request.POST:
course = get_course_with_access(request.user, 'load', course_key)
commentable_ids = get_discussion_categories_ids(course)
commentable_ids = get_discussion_categories_ids(course, request.user)
if request.POST.get("commentable_id") in commentable_ids:
thread.commentable_id = request.POST["commentable_id"]
else:
......
......@@ -2,6 +2,7 @@ import json
import logging
import ddt
from django.core import cache
from django.core.urlresolvers import reverse
from django.http import Http404
from django.test.client import Client, RequestFactory
......@@ -14,7 +15,7 @@ from django_comment_client.tests.group_id import (
NonCohortedTopicGroupIdTestMixin
)
from django_comment_client.tests.unicode import UnicodeTestMixin
from django_comment_client.tests.utils import CohortedContentTestCase
from django_comment_client.tests.utils import CohortedTestCase, ContentGroupTestCase
from django_comment_client.utils import strip_none
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from util.testing import UrlResetMixin
......@@ -186,7 +187,7 @@ class SingleThreadTestCase(ModuleStoreTestCase):
def setUp(self):
super(SingleThreadTestCase, self).setUp(create_user=False)
self.course = CourseFactory.create()
self.course = CourseFactory.create(discussion_topics={'dummy discussion': {'id': 'dummy_discussion_id'}})
self.student = UserFactory.create()
CourseEnrollmentFactory.create(user=self.student, course_id=self.course.id)
......@@ -301,18 +302,24 @@ class SingleThreadQueryCountTestCase(ModuleStoreTestCase):
MODULESTORE = TEST_DATA_MONGO_MODULESTORE
@ddt.data(
# old mongo: number of responses plus 16. TODO: O(n)!
(ModuleStoreEnum.Type.mongo, 1, 17),
(ModuleStoreEnum.Type.mongo, 50, 66),
# old mongo with cache: number of responses plus 17. TODO: O(n)!
(ModuleStoreEnum.Type.mongo, 1, 23, 18),
(ModuleStoreEnum.Type.mongo, 50, 366, 67),
# split mongo: 3 queries, regardless of thread response size.
(ModuleStoreEnum.Type.split, 1, 3),
(ModuleStoreEnum.Type.split, 50, 3),
(ModuleStoreEnum.Type.split, 1, 3, 3),
(ModuleStoreEnum.Type.split, 50, 3, 3),
)
@ddt.unpack
def test_number_of_mongo_queries(self, default_store, num_thread_responses, num_mongo_calls, mock_request):
def test_number_of_mongo_queries(
self,
default_store,
num_thread_responses,
num_uncached_mongo_calls,
num_cached_mongo_calls,
mock_request
):
with modulestore().default_store(default_store):
course = CourseFactory.create()
course = CourseFactory.create(discussion_topics={'dummy discussion': {'id': 'dummy_discussion_id'}})
student = UserFactory.create()
CourseEnrollmentFactory.create(user=student, course_id=course.id)
......@@ -328,7 +335,11 @@ class SingleThreadQueryCountTestCase(ModuleStoreTestCase):
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
)
request.user = student
with check_mongo_calls(num_mongo_calls):
def call_single_thread():
"""
Call single_thread and assert that it returns what we expect.
"""
response = views.single_thread(
request,
course.id.to_deprecated_string(),
......@@ -338,9 +349,30 @@ class SingleThreadQueryCountTestCase(ModuleStoreTestCase):
self.assertEquals(response.status_code, 200)
self.assertEquals(len(json.loads(response.content)["content"]["children"]), num_thread_responses)
# TODO: update this once django cache is disabled in tests
# Test with and without cache, clearing before and after use.
single_thread_local_cache = cache.get_cache(
backend='default',
LOCATION='single_thread_local_cache'
)
single_thread_dummy_cache = cache.get_cache(
backend='django.core.cache.backends.dummy.DummyCache',
LOCATION='single_thread_local_cache'
)
cached_calls = {
single_thread_dummy_cache: num_uncached_mongo_calls,
single_thread_local_cache: num_cached_mongo_calls
}
for single_thread_cache, expected_calls in cached_calls.items():
single_thread_cache.clear()
with patch("django_comment_client.permissions.CACHE", single_thread_cache):
with check_mongo_calls(expected_calls):
call_single_thread()
single_thread_cache.clear()
@patch('requests.request')
class SingleCohortedThreadTestCase(CohortedContentTestCase):
class SingleCohortedThreadTestCase(CohortedTestCase):
def _create_mock_cohorted_thread(self, mock_request):
self.mock_text = "dummy content"
self.mock_thread_id = "test_thread_id"
......@@ -360,7 +392,7 @@ class SingleCohortedThreadTestCase(CohortedContentTestCase):
response = views.single_thread(
request,
self.course.id.to_deprecated_string(),
"dummy_discussion_id",
"cohorted_topic",
self.mock_thread_id
)
......@@ -384,7 +416,7 @@ class SingleCohortedThreadTestCase(CohortedContentTestCase):
response = views.single_thread(
request,
self.course.id.to_deprecated_string(),
"dummy_discussion_id",
"cohorted_topic",
self.mock_thread_id
)
......@@ -397,7 +429,7 @@ class SingleCohortedThreadTestCase(CohortedContentTestCase):
@patch('lms.lib.comment_client.utils.requests.request')
class SingleThreadAccessTestCase(CohortedContentTestCase):
class SingleThreadAccessTestCase(CohortedTestCase):
def call_view(self, mock_request, commentable_id, user, group_id, thread_group_id=None, pass_group_id=True):
thread_id = "test_thread_id"
mock_request.side_effect = make_mock_request_impl("dummy context", thread_id, group_id=thread_group_id)
......@@ -482,7 +514,7 @@ class SingleThreadAccessTestCase(CohortedContentTestCase):
@patch('lms.lib.comment_client.utils.requests.request')
class SingleThreadGroupIdTestCase(CohortedContentTestCase, CohortedTopicGroupIdTestMixin):
class SingleThreadGroupIdTestCase(CohortedTestCase, CohortedTopicGroupIdTestMixin):
cs_endpoint = "/threads"
def call_view(self, mock_request, commentable_id, user, group_id, pass_group_id=True, is_ajax=False):
......@@ -504,7 +536,7 @@ class SingleThreadGroupIdTestCase(CohortedContentTestCase, CohortedTopicGroupIdT
return views.single_thread(
request,
self.course.id.to_deprecated_string(),
"dummy_discussion_id",
commentable_id,
"dummy_thread_id"
)
......@@ -531,9 +563,133 @@ class SingleThreadGroupIdTestCase(CohortedContentTestCase, CohortedTopicGroupIdT
)
@patch('requests.request')
class SingleThreadContentGroupTestCase(ContentGroupTestCase):
def assert_can_access(self, user, discussion_id, thread_id, should_have_access):
"""
Verify that a user has access to a thread within a given
discussion_id when should_have_access is True, otherwise
verify that the user does not have access to that thread.
"""
request = RequestFactory().get("dummy_url")
request.user = user
mako_middleware_process_request(request)
def call_single_thread():
return views.single_thread(
request,
unicode(self.course.id),
discussion_id,
thread_id
)
if should_have_access:
self.assertEqual(call_single_thread().status_code, 200)
else:
with self.assertRaises(Http404):
call_single_thread()
def assert_searched_with_discussion_ids(self, mock_request, expected_commentable_ids):
"""
Verify that the comments service was searched for threads with
the expected discussion ids (passed to the comments service as
'commentable_ids').
"""
mock_request.assert_called_with(
'get',
StringEndsWithMatcher('threads'),
headers=ANY,
timeout=ANY,
data=None,
params=PartialDictMatcher({
'course_id': unicode(self.course.id),
'commentable_ids': ','.join(self.course.top_level_discussion_topic_ids + expected_commentable_ids)
})
)
def test_staff_user(self, mock_request):
"""
Verify that the staff user can access threads in the alpha,
beta, and global discussion modules.
"""
def assert_searched_correct_modules():
self.assert_searched_with_discussion_ids(
mock_request,
[self.beta_module.discussion_id, self.global_module.discussion_id, self.alpha_module.discussion_id]
)
thread_id = "test_thread_id"
mock_request.side_effect = make_mock_request_impl("dummy content", thread_id)
for discussion_module in [self.alpha_module, self.beta_module, self.global_module]:
self.assert_can_access(self.staff_user, discussion_module.discussion_id, thread_id, True)
assert_searched_correct_modules()
def test_alpha_user(self, mock_request):
"""
Verify that the alpha user can access threads in the alpha and
global discussion modules.
"""
def assert_searched_correct_modules():
self.assert_searched_with_discussion_ids(
mock_request,
[self.global_module.discussion_id, self.alpha_module.discussion_id]
)
thread_id = "test_thread_id"
mock_request.side_effect = make_mock_request_impl("dummy content", thread_id)
for discussion_module in [self.alpha_module, self.global_module]:
self.assert_can_access(self.alpha_user, discussion_module.discussion_id, thread_id, True)
assert_searched_correct_modules()
self.assert_can_access(self.alpha_user, self.beta_module.discussion_id, thread_id, False)
def test_beta_user(self, mock_request):
"""
Verify that the beta user can access threads in the beta and
global discussion modules.
"""
def assert_searched_correct_modules():
self.assert_searched_with_discussion_ids(
mock_request,
[self.beta_module.discussion_id, self.global_module.discussion_id]
)
thread_id = "test_thread_id"
mock_request.side_effect = make_mock_request_impl("dummy content", thread_id)
for discussion_module in [self.beta_module, self.global_module]:
self.assert_can_access(self.beta_user, discussion_module.discussion_id, thread_id, True)
assert_searched_correct_modules()
self.assert_can_access(self.beta_user, self.alpha_module.discussion_id, thread_id, False)
def test_non_cohorted_user(self, mock_request):
"""
Verify that the non-cohorted user can access threads in just the
global discussion module.
"""
def assert_searched_correct_modules():
self.assert_searched_with_discussion_ids(
mock_request,
[self.global_module.discussion_id]
)
thread_id = "test_thread_id"
mock_request.side_effect = make_mock_request_impl("dummy content", thread_id)
self.assert_can_access(self.non_cohorted_user, self.global_module.discussion_id, thread_id, True)
assert_searched_correct_modules()
self.assert_can_access(self.non_cohorted_user, self.alpha_module.discussion_id, thread_id, False)
self.assert_can_access(self.non_cohorted_user, self.beta_module.discussion_id, thread_id, False)
@patch('lms.lib.comment_client.utils.requests.request')
class InlineDiscussionGroupIdTestCase(
CohortedContentTestCase,
CohortedTestCase,
CohortedTopicGroupIdTestMixin,
NonCohortedTopicGroupIdTestMixin
):
......@@ -579,7 +735,7 @@ class InlineDiscussionGroupIdTestCase(
@patch('lms.lib.comment_client.utils.requests.request')
class ForumFormDiscussionGroupIdTestCase(CohortedContentTestCase, CohortedTopicGroupIdTestMixin):
class ForumFormDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupIdTestMixin):
cs_endpoint = "/threads"
def call_view(self, mock_request, commentable_id, user, group_id, pass_group_id=True, is_ajax=False):
......@@ -629,7 +785,7 @@ class ForumFormDiscussionGroupIdTestCase(CohortedContentTestCase, CohortedTopicG
@patch('lms.lib.comment_client.utils.requests.request')
class UserProfileDiscussionGroupIdTestCase(CohortedContentTestCase, CohortedTopicGroupIdTestMixin):
class UserProfileDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupIdTestMixin):
cs_endpoint = "/active_threads"
def call_view_for_profiled_user(
......@@ -795,7 +951,7 @@ class UserProfileDiscussionGroupIdTestCase(CohortedContentTestCase, CohortedTopi
@patch('lms.lib.comment_client.utils.requests.request')
class FollowedThreadsDiscussionGroupIdTestCase(CohortedContentTestCase, CohortedTopicGroupIdTestMixin):
class FollowedThreadsDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupIdTestMixin):
cs_endpoint = "/subscribed_threads"
def call_view(self, mock_request, commentable_id, user, group_id, pass_group_id=True):
......@@ -986,7 +1142,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase):
# Invoke UrlResetMixin
super(CommentsServiceRequestHeadersTestCase, self).setUp(create_user=False)
self.course = CourseFactory.create()
self.course = CourseFactory.create(discussion_topics={'dummy discussion': {'id': 'dummy_discussion_id'}})
self.student = UserFactory.create(username=username, password=password)
CourseEnrollmentFactory.create(user=self.student, course_id=self.course.id)
self.assertTrue(
......@@ -1016,7 +1172,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase):
"django_comment_client.forum.views.single_thread",
kwargs={
"course_id": self.course.id.to_deprecated_string(),
"discussion_id": "dummy",
"discussion_id": "dummy_discussion_id",
"thread_id": thread_id,
}
),
......@@ -1110,7 +1266,7 @@ class SingleThreadUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin):
def setUp(self):
super(SingleThreadUnicodeTestCase, self).setUp()
self.course = CourseFactory.create()
self.course = CourseFactory.create(discussion_topics={'dummy_discussion_id': {'id': 'dummy_discussion_id'}})
self.student = UserFactory.create()
CourseEnrollmentFactory(user=self.student, course_id=self.course.id)
......
......@@ -52,7 +52,7 @@ def _attr_safe_json(obj):
@newrelic.agent.function_trace()
def make_course_settings(course):
def make_course_settings(course, user):
"""
Generate a JSON-serializable model for course settings, which will be used to initialize a
DiscussionCourseSettings object on the client.
......@@ -63,14 +63,14 @@ def make_course_settings(course):
'allow_anonymous': course.allow_anonymous,
'allow_anonymous_to_peers': course.allow_anonymous_to_peers,
'cohorts': [{"id": str(g.id), "name": g.name} for g in get_course_cohorts(course)],
'category_map': utils.get_discussion_category_map(course)
'category_map': utils.get_discussion_category_map(course, user)
}
return obj
@newrelic.agent.function_trace()
def get_threads(request, course_key, discussion_id=None, per_page=THREADS_PER_PAGE):
def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE):
"""
This may raise an appropriate subclass of cc.utils.CommentClientError
if something goes wrong, or ValueError if the group_id is invalid.
......@@ -81,12 +81,19 @@ def get_threads(request, course_key, discussion_id=None, per_page=THREADS_PER_PA
'sort_key': 'date',
'sort_order': 'desc',
'text': '',
'commentable_id': discussion_id,
'course_id': course_key.to_deprecated_string(),
'course_id': unicode(course.id),
'user_id': request.user.id,
'group_id': get_group_id_for_comments_service(request, course_key, discussion_id), # may raise ValueError
'group_id': get_group_id_for_comments_service(request, course.id, discussion_id), # may raise ValueError
}
if discussion_id is not None:
default_query_params['commentable_id'] = discussion_id
else:
default_query_params['commentable_ids'] = ','.join(
course.top_level_discussion_topic_ids +
utils.get_discussion_id_map(course, request.user).keys()
)
if not request.GET.get('sort_key'):
# If the user did not select a sort key, use their last used sort key
cc_user = cc.User.from_django_user(request.user)
......@@ -163,7 +170,7 @@ def inline_discussion(request, course_key, discussion_id):
user_info = cc_user.to_dict()
try:
threads, query_params = get_threads(request, course_key, discussion_id, per_page=INLINE_THREADS_PER_PAGE)
threads, query_params = get_threads(request, course, discussion_id, per_page=INLINE_THREADS_PER_PAGE)
except ValueError:
return HttpResponseBadRequest("Invalid group_id")
......@@ -172,7 +179,7 @@ def inline_discussion(request, course_key, discussion_id):
is_staff = cached_has_permission(request.user, 'openclose_thread', course.id)
threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads]
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
add_courseware_context(threads, course)
add_courseware_context(threads, course, request.user)
return utils.JsonResponse({
'is_commentable_cohorted': is_commentable_cohorted(course_key, discussion_id),
'discussion_data': threads,
......@@ -181,7 +188,7 @@ def inline_discussion(request, course_key, discussion_id):
'page': query_params['page'],
'num_pages': query_params['num_pages'],
'roles': utils.get_role_ids(course_key),
'course_settings': make_course_settings(course)
'course_settings': make_course_settings(course, request.user)
})
......@@ -194,13 +201,13 @@ def forum_form_discussion(request, course_key):
nr_transaction = newrelic.agent.current_transaction()
course = get_course_with_access(request.user, 'load_forum', course_key, check_if_enrolled=True)
course_settings = make_course_settings(course)
course_settings = make_course_settings(course, request.user)
user = cc.User.from_django_user(request.user)
user_info = user.to_dict()
try:
unsafethreads, query_params = get_threads(request, course_key) # This might process a search query
unsafethreads, query_params = get_threads(request, course) # This might process a search query
is_staff = cached_has_permission(request.user, 'openclose_thread', course.id)
threads = [utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads]
except cc.utils.CommentClientMaintenanceError:
......@@ -213,7 +220,7 @@ def forum_form_discussion(request, course_key):
annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info)
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
add_courseware_context(threads, course)
add_courseware_context(threads, course, request.user)
if request.is_ajax():
return utils.JsonResponse({
......@@ -258,15 +265,21 @@ 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()
course = get_course_with_access(request.user, 'load_forum', course_key)
course_settings = make_course_settings(course)
course_settings = make_course_settings(course, request.user)
cc_user = cc.User.from_django_user(request.user)
user_info = cc_user.to_dict()
is_moderator = cached_has_permission(request.user, "see_all_cohorts", course_key)
# Verify that the student has access to this thread if belongs to a discussion module
accessible_discussion_ids = [
module.discussion_id for module in utils.get_accessible_discussion_modules(course, request.user)
]
if discussion_id not in set(course.top_level_discussion_topic_ids + accessible_discussion_ids):
raise Http404
# Currently, the front end always loads responses via AJAX, even for this
# page; it would be a nice optimization to avoid that extra round trip to
# the comments service.
......@@ -294,7 +307,7 @@ def single_thread(request, course_key, discussion_id, thread_id):
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)
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
add_courseware_context([content], course)
add_courseware_context([content], course, request.user)
return utils.JsonResponse({
'content': content,
'annotated_content_info': annotated_content_info,
......@@ -302,13 +315,13 @@ def single_thread(request, course_key, discussion_id, thread_id):
else:
try:
threads, query_params = get_threads(request, course_key)
threads, query_params = get_threads(request, course)
except ValueError:
return HttpResponseBadRequest("Invalid group_id")
threads.append(thread.to_dict())
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
add_courseware_context(threads, course)
add_courseware_context(threads, course, request.user)
for thread in threads:
# patch for backward compatibility with comments service
......
......@@ -5,13 +5,12 @@ from pytz import UTC
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.utils import override_settings
from edxmako import add_lookup
import mock
from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE
from django_comment_client.tests.factories import RoleFactory
from django_comment_client.tests.unicode import UnicodeTestMixin
from django_comment_client.tests.utils import ContentGroupTestCase
import django_comment_client.utils as utils
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
......@@ -90,7 +89,7 @@ class CoursewareContextTestCase(ModuleStoreTestCase):
comment client service integration
"""
def setUp(self):
super(CoursewareContextTestCase, self).setUp()
super(CoursewareContextTestCase, self).setUp(create_user=True)
self.course = CourseFactory.create(org="TestX", number="101", display_name="Test Course")
self.discussion1 = ItemFactory.create(
......@@ -109,12 +108,12 @@ class CoursewareContextTestCase(ModuleStoreTestCase):
)
def test_empty(self):
utils.add_courseware_context([], self.course)
utils.add_courseware_context([], self.course, self.user)
def test_missing_commentable_id(self):
orig = {"commentable_id": "non-inline"}
modified = dict(orig)
utils.add_courseware_context([modified], self.course)
utils.add_courseware_context([modified], self.course, self.user)
self.assertEqual(modified, orig)
def test_basic(self):
......@@ -122,7 +121,7 @@ class CoursewareContextTestCase(ModuleStoreTestCase):
{"commentable_id": self.discussion1.discussion_id},
{"commentable_id": self.discussion2.discussion_id}
]
utils.add_courseware_context(threads, self.course)
utils.add_courseware_context(threads, self.course, self.user)
def assertThreadCorrect(thread, discussion, expected_title): # pylint: disable=invalid-name
"""Asserts that the given thread has the expected set of properties"""
......@@ -146,13 +145,29 @@ class CoursewareContextTestCase(ModuleStoreTestCase):
assertThreadCorrect(threads[1], self.discussion2, "Subsection / Discussion 2")
class CategoryMapTestCase(ModuleStoreTestCase):
class CategoryMapTestMixin(object):
"""
Provides functionality for classes that test
`get_discussion_category_map`.
"""
def assert_category_map_equals(self, expected, requesting_user=None):
"""
Call `get_discussion_category_map`, and verify that it returns
what is expected.
"""
self.assertEqual(
utils.get_discussion_category_map(self.course, requesting_user or self.user),
expected
)
class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
"""
Base testcase class for discussion categories for the
comment client service integration
"""
def setUp(self):
super(CategoryMapTestCase, self).setUp()
super(CategoryMapTestCase, self).setUp(create_user=True)
self.course = CourseFactory.create(
org="TestX", number="101", display_name="Test Course",
......@@ -178,17 +193,8 @@ class CategoryMapTestCase(ModuleStoreTestCase):
**kwargs
)
def assertCategoryMapEquals(self, expected):
self.assertEqual(
utils.get_discussion_category_map(self.course),
expected
)
def test_empty(self):
self.assertEqual(
utils.get_discussion_category_map(self.course),
{"entries": {}, "subcategories": {}, "children": []}
)
self.assert_category_map_equals({"entries": {}, "subcategories": {}, "children": []})
def test_configured_topics(self):
self.course.discussion_topics = {
......@@ -198,7 +204,7 @@ class CategoryMapTestCase(ModuleStoreTestCase):
}
def check_cohorted_topics(expected_ids):
self.assertCategoryMapEquals(
self.assert_category_map_equals(
{
"entries": {
"Topic A": {"id": "Topic_A", "sort_key": "Topic A", "is_cohorted": "Topic_A" in expected_ids},
......@@ -230,7 +236,7 @@ class CategoryMapTestCase(ModuleStoreTestCase):
def test_single_inline(self):
self.create_discussion("Chapter", "Discussion")
self.assertCategoryMapEquals(
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
......@@ -260,7 +266,7 @@ class CategoryMapTestCase(ModuleStoreTestCase):
def check_cohorted(is_cohorted):
self.assertCategoryMapEquals(
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
......@@ -363,7 +369,7 @@ class CategoryMapTestCase(ModuleStoreTestCase):
self.create_discussion("Chapter 2 / Section 1 / Subsection 2", "Discussion", start=later)
self.create_discussion("Chapter 3 / Section 1", "Discussion", start=later)
self.assertCategoryMapEquals(
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
......@@ -401,7 +407,7 @@ class CategoryMapTestCase(ModuleStoreTestCase):
self.create_discussion("Chapter", "Discussion 4", sort_key="C")
self.create_discussion("Chapter", "Discussion 5", sort_key="B")
self.assertCategoryMapEquals(
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
......@@ -453,7 +459,7 @@ class CategoryMapTestCase(ModuleStoreTestCase):
"Topic B": {"id": "Topic_B", "sort_key": "C"},
"Topic C": {"id": "Topic_C", "sort_key": "A"}
}
self.assertCategoryMapEquals(
self.assert_category_map_equals(
{
"entries": {
"Topic A": {"id": "Topic_A", "sort_key": "B", "is_cohorted": False},
......@@ -474,7 +480,7 @@ class CategoryMapTestCase(ModuleStoreTestCase):
self.create_discussion("Chapter", "Discussion C")
self.create_discussion("Chapter", "Discussion B")
self.assertCategoryMapEquals(
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
......@@ -527,7 +533,7 @@ class CategoryMapTestCase(ModuleStoreTestCase):
self.create_discussion("Chapter B", "Discussion 1")
self.create_discussion("Chapter A", "Discussion 2")
self.assertCategoryMapEquals(
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
......@@ -580,7 +586,7 @@ class CategoryMapTestCase(ModuleStoreTestCase):
)
def test_ids_empty(self):
self.assertEqual(utils.get_discussion_categories_ids(self.course), [])
self.assertEqual(utils.get_discussion_categories_ids(self.course, self.user), [])
def test_ids_configured_topics(self):
self.course.discussion_topics = {
......@@ -589,7 +595,7 @@ class CategoryMapTestCase(ModuleStoreTestCase):
"Topic C": {"id": "Topic_C"}
}
self.assertItemsEqual(
utils.get_discussion_categories_ids(self.course),
utils.get_discussion_categories_ids(self.course, self.user),
["Topic_A", "Topic_B", "Topic_C"]
)
......@@ -601,7 +607,7 @@ class CategoryMapTestCase(ModuleStoreTestCase):
self.create_discussion("Chapter 2 / Section 1 / Subsection 2", "Discussion")
self.create_discussion("Chapter 3 / Section 1", "Discussion")
self.assertItemsEqual(
utils.get_discussion_categories_ids(self.course),
utils.get_discussion_categories_ids(self.course, self.user),
["discussion1", "discussion2", "discussion3", "discussion4", "discussion5", "discussion6"]
)
......@@ -615,11 +621,177 @@ class CategoryMapTestCase(ModuleStoreTestCase):
self.create_discussion("Chapter 2", "Discussion")
self.create_discussion("Chapter 2 / Section 1 / Subsection 1", "Discussion")
self.assertItemsEqual(
utils.get_discussion_categories_ids(self.course),
utils.get_discussion_categories_ids(self.course, self.user),
["Topic_A", "Topic_B", "Topic_C", "discussion1", "discussion2", "discussion3"]
)
class ContentGroupCategoryMapTestCase(CategoryMapTestMixin, ContentGroupTestCase):
"""
Tests `get_discussion_category_map` on discussion modules which are
only visible to some content groups.
"""
def test_staff_user(self):
"""
Verify that the staff user can access the alpha, beta, and
global discussion topics.
"""
self.assert_category_map_equals(
{
'subcategories': {
'Week 1': {
'subcategories': {},
'children': [
'Visible to Alpha',
'Visible to Beta',
'Visible to Everyone'
],
'entries': {
'Visible to Alpha': {
'sort_key': None,
'is_cohorted': True,
'id': 'alpha_group_discussion'
},
'Visible to Beta': {
'sort_key': None,
'is_cohorted': True,
'id': 'beta_group_discussion'
},
'Visible to Everyone': {
'sort_key': None,
'is_cohorted': True,
'id': 'global_group_discussion'
}
}
}
},
'children': ['General', 'Week 1'],
'entries': {
'General': {
'sort_key': 'General',
'is_cohorted': False,
'id': 'i4x-org-number-course-run'
}
}
},
requesting_user=self.staff_user
)
def test_alpha_user(self):
"""
Verify that the alpha user can access the alpha and global
discussion topics.
"""
self.assert_category_map_equals(
{
'subcategories': {
'Week 1': {
'subcategories': {},
'children': [
'Visible to Alpha',
'Visible to Everyone'
],
'entries': {
'Visible to Alpha': {
'sort_key': None,
'is_cohorted': True,
'id': 'alpha_group_discussion'
},
'Visible to Everyone': {
'sort_key': None,
'is_cohorted': True,
'id': 'global_group_discussion'
}
}
}
},
'children': ['General', 'Week 1'],
'entries': {
'General': {
'sort_key': 'General',
'is_cohorted': False,
'id': 'i4x-org-number-course-run'
}
}
},
requesting_user=self.alpha_user
)
def test_beta_user(self):
"""
Verify that the beta user can access the beta and global
discussion topics.
"""
self.assert_category_map_equals(
{
'subcategories': {
'Week 1': {
'subcategories': {},
'children': [
'Visible to Beta',
'Visible to Everyone'
],
'entries': {
'Visible to Beta': {
'sort_key': None,
'is_cohorted': True,
'id': 'beta_group_discussion'
},
'Visible to Everyone': {
'sort_key': None,
'is_cohorted': True,
'id': 'global_group_discussion'
}
}
}
},
'children': ['General', 'Week 1'],
'entries': {
'General': {
'sort_key': 'General',
'is_cohorted': False,
'id': 'i4x-org-number-course-run'
}
}
},
requesting_user=self.beta_user
)
def test_non_cohorted_user(self):
"""
Verify that the non-cohorted user can access the global
discussion topic.
"""
self.assert_category_map_equals(
{
'subcategories': {
'Week 1': {
'subcategories': {},
'children': [
'Visible to Everyone'
],
'entries': {
'Visible to Everyone': {
'sort_key': None,
'is_cohorted': True,
'id': 'global_group_discussion'
}
}
}
},
'children': ['General', 'Week 1'],
'entries': {
'General': {
'sort_key': 'General',
'is_cohorted': False,
'id': 'i4x-org-number-course-run'
}
}
},
requesting_user=self.non_cohorted_user
)
class JsonResponseTestCase(TestCase, UnicodeTestMixin):
def _test_unicode_data(self, text):
response = utils.JsonResponse(text)
......
"""
Utilities for tests within the django_comment_client module.
"""
from datetime import datetime
from django.test.utils import override_settings
from mock import patch
from pytz import UTC
from openedx.core.djangoapps.course_groups.models import CourseUserGroup
from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE
from openedx.core.djangoapps.course_groups.models import CourseUserGroupPartitionGroup
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
from django_comment_common.models import Role
from django_comment_common.utils import seed_permissions_roles
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.partitions.partitions import UserPartition, Group
class CohortedContentTestCase(ModuleStoreTestCase):
class CohortedTestCase(ModuleStoreTestCase):
"""
Sets up a course with a student, a moderator and their cohorts.
"""
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def setUp(self):
super(CohortedContentTestCase, self).setUp()
super(CohortedTestCase, self).setUp()
self.course = CourseFactory.create(
discussion_topics={
......@@ -28,15 +34,13 @@ class CohortedContentTestCase(ModuleStoreTestCase):
"cohorted_discussions": ["cohorted_topic"]
}
)
self.student_cohort = CourseUserGroup.objects.create(
self.student_cohort = CohortFactory.create(
name="student_cohort",
course_id=self.course.id,
group_type=CourseUserGroup.COHORT
course_id=self.course.id
)
self.moderator_cohort = CourseUserGroup.objects.create(
self.moderator_cohort = CohortFactory.create(
name="moderator_cohort",
course_id=self.course.id,
group_type=CourseUserGroup.COHORT
course_id=self.course.id
)
seed_permissions_roles(self.course.id)
......@@ -47,3 +51,82 @@ class CohortedContentTestCase(ModuleStoreTestCase):
self.moderator.roles.add(Role.objects.get(name="Moderator", course_id=self.course.id))
self.student_cohort.users.add(self.student)
self.moderator_cohort.users.add(self.moderator)
class ContentGroupTestCase(ModuleStoreTestCase):
"""
Sets up discussion modules visible to content groups 'Alpha' and
'Beta', as well as a module visible to all students. Creates a
staff user, users with access to Alpha/Beta (by way of cohorts),
and a non-cohorted user with no special access.
"""
def setUp(self):
super(ContentGroupTestCase, self).setUp()
self.course = CourseFactory.create(
org='org', number='number', run='run',
# This test needs to use a course that has already started --
# discussion topics only show up if the course has already started,
# and the default start date for courses is Jan 1, 2030.
start=datetime(2012, 2, 3, tzinfo=UTC),
user_partitions=[
UserPartition(
0,
'Content Group Configuration',
'',
[Group(1, 'Alpha'), Group(2, 'Beta')],
scheme_id='cohort'
)
],
cohort_config={'cohorted': True},
discussion_topics={}
)
self.staff_user = UserFactory.create(is_staff=True)
self.alpha_user = UserFactory.create()
self.beta_user = UserFactory.create()
self.non_cohorted_user = UserFactory.create()
for user in [self.staff_user, self.alpha_user, self.beta_user, self.non_cohorted_user]:
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
alpha_cohort = CohortFactory(
course_id=self.course.id,
name='Cohort Alpha',
users=[self.alpha_user]
)
beta_cohort = CohortFactory(
course_id=self.course.id,
name='Cohort Beta',
users=[self.beta_user]
)
CourseUserGroupPartitionGroup.objects.create(
course_user_group=alpha_cohort,
partition_id=self.course.user_partitions[0].id,
group_id=self.course.user_partitions[0].groups[0].id
)
CourseUserGroupPartitionGroup.objects.create(
course_user_group=beta_cohort,
partition_id=self.course.user_partitions[0].id,
group_id=self.course.user_partitions[0].groups[1].id
)
self.alpha_module = ItemFactory.create(
parent_location=self.course.location,
category='discussion',
discussion_id='alpha_group_discussion',
discussion_target='Visible to Alpha',
group_access={self.course.user_partitions[0].id: [self.course.user_partitions[0].groups[0].id]}
)
self.beta_module = ItemFactory.create(
parent_location=self.course.location,
category='discussion',
discussion_id='beta_group_discussion',
discussion_target='Visible to Beta',
group_access={self.course.user_partitions[0].id: [self.course.user_partitions[0].groups[1].id]}
)
self.global_module = ItemFactory.create(
parent_location=self.course.location,
category='discussion',
discussion_id='global_group_discussion',
discussion_target='Visible to Everyone'
)
self.course = self.store.get_item(self.course.location)
......@@ -17,6 +17,7 @@ from django_comment_client.permissions import check_permissions_by_view, cached_
from edxmako import lookup_template
import pystache_custom as pystache
from courseware.access import has_access
from openedx.core.djangoapps.course_groups.cohorts import get_cohort_by_id, get_cohort_id, is_commentable_cohorted, is_course_cohorted
from openedx.core.djangoapps.course_groups.models import CourseUserGroup
from opaque_keys.edx.locations import i4xEncoder
......@@ -59,7 +60,12 @@ def has_forum_access(uname, course_id, rolename):
return role.users.filter(username=uname).exists()
def _get_discussion_modules(course):
# pylint: disable=invalid-name
def get_accessible_discussion_modules(course, user):
"""
Get all discussion modules within a course which are accessible to
the user.
"""
all_modules = modulestore().get_items(course.id, qualifiers={'category': 'discussion'})
def has_required_keys(module):
......@@ -69,17 +75,23 @@ def _get_discussion_modules(course):
return False
return True
return filter(has_required_keys, all_modules)
return [
module for module in all_modules
if has_required_keys(module) and has_access(user, 'load', module, course.id)
]
def get_discussion_id_map(course):
def get_discussion_id_map(course, user):
"""
Get metadata about discussion modules visible to the user in a course.
"""
def get_entry(module):
discussion_id = module.discussion_id
title = module.discussion_target
last_category = module.discussion_category.split("/")[-1].strip()
return (discussion_id, {"location": module.location, "title": last_category + " / " + title})
return dict(map(get_entry, _get_discussion_modules(course)))
return dict(map(get_entry, get_accessible_discussion_modules(course, user)))
def _filter_unstarted_categories(category_map):
......@@ -132,12 +144,14 @@ def _sort_map_entries(category_map, sort_alpha):
category_map["children"] = [x[0] for x in sorted(things, key=lambda x: x[1]["sort_key"])]
def get_discussion_category_map(course):
course_id = course.id
def get_discussion_category_map(course, user):
"""
Get a mapping of categories and subcategories that are visible to
the user within a course.
"""
unexpanded_category_map = defaultdict(list)
modules = _get_discussion_modules(course)
modules = get_accessible_discussion_modules(course, user)
is_course_cohorted = course.is_cohorted
cohorted_discussion_ids = course.cohorted_discussions
......@@ -203,12 +217,12 @@ def get_discussion_category_map(course):
return _filter_unstarted_categories(category_map)
def get_discussion_categories_ids(course):
def get_discussion_categories_ids(course, user):
"""
Returns a list of available ids of categories for the course.
"""
ids = []
queue = [get_discussion_category_map(course)]
queue = [get_discussion_category_map(course, user)]
while queue:
category_map = queue.pop()
for child in category_map["children"]:
......@@ -369,8 +383,11 @@ def extend_content(content):
return merge_dict(content, content_info)
def add_courseware_context(content_list, course):
id_map = get_discussion_id_map(course)
def add_courseware_context(content_list, course, user):
"""
Decorates `content_list` with courseware metadata.
"""
id_map = get_discussion_id_map(course, user)
for content in content_list:
commentable_id = content['commentable_id']
......
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