""" Tests for the discussion xblock. Most of the tests are in common/xblock/xblock_discussion, here are only tests for functionalities that require django API, and lms specific functionalities. """ import json import uuid import ddt import mock from django.core.urlresolvers import reverse from xblock.field_data import DictFieldData from xblock.fragment import Fragment from course_api.blocks.tests.helpers import deserialize_usage_key from courseware.module_render import get_module_for_descriptor_internal from lms.djangoapps.courseware.tests.helpers import XModuleRenderingTestBase from student.tests.factories import CourseEnrollmentFactory, UserFactory from xblock_discussion import DiscussionXBlock, loader from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import ItemFactory, ToyCourseFactory @ddt.ddt class TestDiscussionXBlock(XModuleRenderingTestBase): """ Base class for tests """ PATCH_DJANGO_USER = True def setUp(self): """ Set up the xblock runtime, test course, discussion, and user. """ super(TestDiscussionXBlock, self).setUp() self.patchers = [] self.course_id = "test_course" self.runtime = self.new_module_runtime() self.runtime.modulestore = mock.Mock() self.discussion_id = str(uuid.uuid4()) self.data = DictFieldData({ 'discussion_id': self.discussion_id }) scope_ids = mock.Mock() scope_ids.usage_id.course_key = self.course_id self.block = DiscussionXBlock( self.runtime, field_data=self.data, scope_ids=scope_ids ) self.block.xmodule_runtime = mock.Mock() if self.PATCH_DJANGO_USER: self.django_user_canary = object() self.django_user_mock = self.add_patcher( mock.patch.object(DiscussionXBlock, "django_user", new_callable=mock.PropertyMock) ) self.django_user_mock.return_value = self.django_user_canary def add_patcher(self, patcher): """ Registers a patcher object, and returns mock. This patcher will be disabled after the test. """ self.patchers.append(patcher) return patcher.start() def tearDown(self): """ Tears down any patchers added during tests. """ super(TestDiscussionXBlock, self).tearDown() for patcher in self.patchers: patcher.stop() class TestGetDjangoUser(TestDiscussionXBlock): """ Tests for the django_user property. """ PATCH_DJANGO_USER = False def setUp(self): """ Mock the user service and runtime. """ super(TestGetDjangoUser, self).setUp() self.django_user = object() self.user_service = mock.Mock() self.add_patcher( mock.patch.object(self.runtime, "service", return_value=self.user_service) ) self.user_service._django_user = self.django_user # pylint: disable=protected-access def test_django_user(self): """ Tests that django_user users returns _django_user attribute of the user service. """ actual_user = self.block.django_user self.runtime.service.assert_called_once_with( self.block, 'user') self.assertEqual(actual_user, self.django_user) def test_django_user_handles_missing_service(self): """ Tests that get_django gracefully handles missing user service. """ self.runtime.service.return_value = None self.assertEqual(self.block.django_user, None) @ddt.ddt class TestViews(TestDiscussionXBlock): """ Tests for student_view and author_view. """ def setUp(self): """ Mock the methods needed for these tests. """ super(TestViews, self).setUp() self.template_canary = u'canary' self.render_template = mock.Mock() self.render_template.return_value = self.template_canary self.block.runtime.render_template = self.render_template self.has_permission_mock = mock.Mock() self.has_permission_mock.return_value = False self.block.has_permission = self.has_permission_mock def get_template_context(self): """ Returns context passed to rendering of the django template (rendered by runtime). """ self.assertEqual(self.render_template.call_count, 1) return self.render_template.call_args_list[0][0][1] def get_rendered_template(self): """ Returns the name of the template rendered by runtime. """ self.assertEqual(self.render_template.call_count, 1) return self.render_template.call_args_list[0][0][0] def test_studio_view(self): """ Test for the studio view. """ fragment = self.block.author_view() self.assertIsInstance(fragment, Fragment) self.assertEqual(fragment.content, self.template_canary) self.render_template.assert_called_once_with( 'discussion/_discussion_inline_studio.html', {'discussion_id': self.discussion_id} ) @ddt.data( (False, False, False), (True, False, False), (False, True, False), (False, False, True), ) def test_student_perms_are_correct(self, permissions): """ Test that context will get proper permissions. """ permission_dict = { 'create_thread': permissions[0], 'create_comment': permissions[1], 'create_sub_comment': permissions[2] } expected_permissions = { 'can_create_thread': permission_dict['create_thread'], 'can_create_comment': permission_dict['create_comment'], 'can_create_subcomment': permission_dict['create_sub_comment'], } self.block.has_permission = lambda perm: permission_dict[perm] with mock.patch.object(loader, 'render_template', mock.Mock): self.block.student_view() context = self.get_template_context() for permission_name, expected_value in expected_permissions.items(): self.assertEqual(expected_value, context[permission_name]) def test_js_init(self): """ Test proper js init function is called. """ with mock.patch.object(loader, 'render_template', mock.Mock): fragment = self.block.student_view() self.assertEqual(fragment.js_init_fn, 'DiscussionInlineBlock') @ddt.ddt class TestTemplates(TestDiscussionXBlock): """ Tests rendering of templates. """ def test_has_permission(self): """ Test for has_permission method. """ permission_canary = object() with mock.patch('django_comment_client.permissions.has_permission', return_value=permission_canary) as has_perm: actual_permission = self.block.has_permission("test_permission") self.assertEqual(actual_permission, permission_canary) has_perm.assert_called_once_with(self.django_user_canary, 'test_permission', 'test_course') def test_studio_view(self): """Test for studio view.""" fragment = self.block.author_view({}) self.assertIn('data-discussion-id="{}"'.format(self.discussion_id), fragment.content) @ddt.data( (True, False, False), (False, True, False), (False, False, True), ) def test_student_perms_are_correct(self, permissions): """ Test for lms view. """ permission_dict = { 'create_thread': permissions[0], 'create_comment': permissions[1], 'create_sub_comment': permissions[2] } self.block.has_permission = lambda perm: permission_dict[perm] fragment = self.block.student_view() read_only = 'false' if permissions[0] else 'true' self.assertIn('data-discussion-id="{}"'.format(self.discussion_id), fragment.content) self.assertIn('data-user-create-comment="{}"'.format(json.dumps(permissions[1])), fragment.content) self.assertIn('data-user-create-subcomment="{}"'.format(json.dumps(permissions[2])), fragment.content) self.assertIn('data-read-only="{read_only}"'.format(read_only=read_only), fragment.content) @ddt.ddt class TestXBlockInCourse(SharedModuleStoreTestCase): """ Test the discussion xblock as rendered in the course and course API. """ @classmethod def setUpClass(cls): """ Set up a user, course, and discussion XBlock for use by tests. """ super(TestXBlockInCourse, cls).setUpClass() cls.user = UserFactory.create() cls.course = ToyCourseFactory.create() cls.course_key = cls.course.id cls.course_usage_key = cls.store.make_course_usage_key(cls.course_key) cls.discussion_id = "test_discussion_xblock_id" cls.discussion = ItemFactory.create( parent_location=cls.course_usage_key, category='discussion', discussion_id=cls.discussion_id, discussion_category='Category discussion', discussion_target='Target Discussion', ) CourseEnrollmentFactory.create(user=cls.user, course_id=cls.course_key) def get_root(self, block): """ Return root of the block. """ while block.parent: block = block.get_parent() return block def test_html_with_user(self): """ Test rendered DiscussionXBlock permissions. """ discussion_xblock = get_module_for_descriptor_internal( user=self.user, descriptor=self.discussion, student_data=mock.Mock(name='student_data'), course_id=self.course.id, track_function=mock.Mock(name='track_function'), xqueue_callback_url_prefix=mock.Mock(name='xqueue_callback_url_prefix'), request_token='request_token', ) fragment = discussion_xblock.render('student_view') html = fragment.content self.assertIn('data-user-create-comment="false"', html) self.assertIn('data-user-create-subcomment="false"', html) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_discussion_render_successfully_with_orphan_parent(self, default_store): """ Test that discussion xblock render successfully if discussion xblock is child of an orphan. """ with self.store.default_store(default_store): orphan_sequential = self.store.create_item(self.user.id, self.course.id, 'sequential') vertical = self.store.create_child( self.user.id, orphan_sequential.location, 'vertical', block_id=self.course.location.block_id ) discussion = self.store.create_child( self.user.id, vertical.location, 'discussion', block_id=self.course.location.block_id ) discussion = self.store.get_item(discussion.location) root = self.get_root(discussion) # Assert that orphan sequential is root of the discussion xblock. self.assertEqual(orphan_sequential.location.block_type, root.location.block_type) self.assertEqual(orphan_sequential.location.block_id, root.location.block_id) # Get xblock bound to a user and a descriptor. discussion_xblock = get_module_for_descriptor_internal( user=self.user, descriptor=discussion, student_data=mock.Mock(name='student_data'), course_id=self.course.id, track_function=mock.Mock(name='track_function'), xqueue_callback_url_prefix=mock.Mock(name='xqueue_callback_url_prefix'), request_token='request_token', ) fragment = discussion_xblock.render('student_view') html = fragment.content self.assertIsInstance(discussion_xblock, DiscussionXBlock) self.assertIn('data-user-create-comment="false"', html) self.assertIn('data-user-create-subcomment="false"', html) def test_discussion_student_view_data(self): """ Tests that course block api returns student_view_data for discussion xblock """ self.client.login(username=self.user.username, password='test') url = reverse('blocks_in_block_tree', kwargs={'usage_key_string': unicode(self.course_usage_key)}) query_params = { 'depth': 'all', 'username': self.user.username, 'block_types_filter': 'discussion', 'student_view_data': 'discussion' } response = self.client.get(url, query_params) self.assertEquals(response.status_code, 200) self.assertEquals(response.data['root'], unicode(self.course_usage_key)) # pylint: disable=no-member for block_key_string, block_data in response.data['blocks'].iteritems(): # pylint: disable=no-member block_key = deserialize_usage_key(block_key_string, self.course_key) self.assertEquals(block_data['id'], block_key_string) self.assertEquals(block_data['type'], block_key.block_type) self.assertEquals(block_data['display_name'], self.store.get_item(block_key).display_name or '') self.assertEqual(block_data['student_view_data'], {"topic_id": self.discussion_id}) class TestXBlockQueryLoad(SharedModuleStoreTestCase): """ Test the number of queries executed when rendering the XBlock. """ def test_permissions_query_load(self): """ Tests that the permissions queries are cached when rendering numerous discussion XBlocks. """ user = UserFactory.create() course = ToyCourseFactory.create() course_key = course.id course_usage_key = self.store.make_course_usage_key(course_key) discussions = [] for counter in range(5): discussion_id = 'test_discussion_{}'.format(counter) discussions.append(ItemFactory.create( parent_location=course_usage_key, category='discussion', discussion_id=discussion_id, discussion_category='Category discussion', discussion_target='Target Discussion', )) # 3 queries are required to do first discussion xblock render: # * django_comment_client_role # * django_comment_client_permission # * lms_xblock_xblockasidesconfig num_queries = 3 for discussion in discussions: discussion_xblock = get_module_for_descriptor_internal( user=user, descriptor=discussion, student_data=mock.Mock(name='student_data'), course_id=course.id, track_function=mock.Mock(name='track_function'), xqueue_callback_url_prefix=mock.Mock(name='xqueue_callback_url_prefix'), request_token='request_token', ) with self.assertNumQueries(num_queries): fragment = discussion_xblock.render('student_view') # Permissions are cached, so no queries required for subsequent renders num_queries = 0 html = fragment.content self.assertIn('data-user-create-comment="false"', html) self.assertIn('data-user-create-subcomment="false"', html)