Files
edx-platform/lms/djangoapps/django_comment_client/tests/test_utils.py

1862 lines
78 KiB
Python

# -*- coding: utf-8 -*-
import datetime
import json
import ddt
import mock
import pytest
from django.core.urlresolvers import reverse
from django.test import RequestFactory, TestCase
from mock import Mock, patch
from nose.plugins.attrib import attr
from pytz import UTC
import django_comment_client.utils as utils
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from courseware.tabs import get_course_tab_list
from courseware.tests.factories import InstructorFactory
from django_comment_client.constants import TYPE_ENTRY, TYPE_SUBCATEGORY
from django_comment_client.tests.factories import RoleFactory
from django_comment_client.tests.unicode import UnicodeTestMixin
from django_comment_client.tests.utils import config_course_discussions, topic_name_to_id
from django_comment_common.models import (
CourseDiscussionSettings,
ForumsConfig,
assign_role
)
from django_comment_common.utils import (
get_course_discussion_settings,
seed_permissions_roles,
set_course_discussion_settings
)
from lms.djangoapps.teams.tests.factories import CourseTeamFactory
from lms.lib.comment_client.utils import CommentClientMaintenanceError, perform_request
from openedx.core.djangoapps.content.course_structures.models import CourseStructure
from openedx.core.djangoapps.course_groups import cohorts
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory, config_course_cohorts
from openedx.core.djangoapps.util.testing import ContentGroupTestCase
from student.roles import CourseStaffRole
from student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_MODULESTORE, ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, ToyCourseFactory
@attr(shard=1)
class DictionaryTestCase(TestCase):
def test_extract(self):
d = {'cats': 'meow', 'dogs': 'woof'}
k = ['cats', 'dogs', 'hamsters']
expected = {'cats': 'meow', 'dogs': 'woof', 'hamsters': None}
self.assertEqual(utils.extract(d, k), expected)
def test_strip_none(self):
d = {'cats': 'meow', 'dogs': 'woof', 'hamsters': None}
expected = {'cats': 'meow', 'dogs': 'woof'}
self.assertEqual(utils.strip_none(d), expected)
def test_strip_blank(self):
d = {'cats': 'meow', 'dogs': 'woof', 'hamsters': ' ', 'yetis': ''}
expected = {'cats': 'meow', 'dogs': 'woof'}
self.assertEqual(utils.strip_blank(d), expected)
def test_merge_dict(self):
d1 = {'cats': 'meow', 'dogs': 'woof'}
d2 = {'lions': 'roar', 'ducks': 'quack'}
expected = {'cats': 'meow', 'dogs': 'woof', 'lions': 'roar', 'ducks': 'quack'}
self.assertEqual(utils.merge_dict(d1, d2), expected)
@attr(shard=1)
@pytest.mark.django111_expected_failure
class AccessUtilsTestCase(ModuleStoreTestCase):
"""
Base testcase class for access and roles for the
comment client service integration
"""
CREATE_USER = False
def setUp(self):
super(AccessUtilsTestCase, self).setUp()
self.course = CourseFactory.create()
self.course_id = self.course.id
self.student_role = RoleFactory(name='Student', course_id=self.course_id)
self.moderator_role = RoleFactory(name='Moderator', course_id=self.course_id)
self.community_ta_role = RoleFactory(name='Community TA', course_id=self.course_id)
self.student1 = UserFactory(username='student', email='student@edx.org')
self.student1_enrollment = CourseEnrollmentFactory(user=self.student1)
self.student_role.users.add(self.student1)
self.student2 = UserFactory(username='student2', email='student2@edx.org')
self.student2_enrollment = CourseEnrollmentFactory(user=self.student2)
self.moderator = UserFactory(username='moderator', email='staff@edx.org', is_staff=True)
self.moderator_enrollment = CourseEnrollmentFactory(user=self.moderator)
self.moderator_role.users.add(self.moderator)
self.community_ta1 = UserFactory(username='community_ta1', email='community_ta1@edx.org')
self.community_ta_role.users.add(self.community_ta1)
self.community_ta2 = UserFactory(username='community_ta2', email='community_ta2@edx.org')
self.community_ta_role.users.add(self.community_ta2)
self.course_staff = UserFactory(username='course_staff', email='course_staff@edx.org')
CourseStaffRole(self.course_id).add_users(self.course_staff)
def test_get_role_ids(self):
ret = utils.get_role_ids(self.course_id)
expected = {u'Moderator': [3], u'Community TA': [4, 5]}
self.assertEqual(ret, expected)
def test_has_discussion_privileges(self):
self.assertFalse(utils.has_discussion_privileges(self.student1, self.course_id))
self.assertFalse(utils.has_discussion_privileges(self.student2, self.course_id))
self.assertFalse(utils.has_discussion_privileges(self.course_staff, self.course_id))
self.assertTrue(utils.has_discussion_privileges(self.moderator, self.course_id))
self.assertTrue(utils.has_discussion_privileges(self.community_ta1, self.course_id))
self.assertTrue(utils.has_discussion_privileges(self.community_ta2, self.course_id))
def test_has_forum_access(self):
ret = utils.has_forum_access('student', self.course_id, 'Student')
self.assertTrue(ret)
ret = utils.has_forum_access('not_a_student', self.course_id, 'Student')
self.assertFalse(ret)
ret = utils.has_forum_access('student', self.course_id, 'NotARole')
self.assertFalse(ret)
@ddt.ddt
@attr(shard=1)
class CoursewareContextTestCase(ModuleStoreTestCase):
"""
Base testcase class for courseware context for the
comment client service integration
"""
def setUp(self):
super(CoursewareContextTestCase, self).setUp()
self.course = CourseFactory.create(org="TestX", number="101", display_name="Test Course")
self.discussion1 = ItemFactory.create(
parent_location=self.course.location,
category="discussion",
discussion_id="discussion1",
discussion_category="Chapter",
discussion_target="Discussion 1"
)
self.discussion2 = ItemFactory.create(
parent_location=self.course.location,
category="discussion",
discussion_id="discussion2",
discussion_category="Chapter / Section / Subsection",
discussion_target="Discussion 2"
)
def test_empty(self):
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, self.user)
self.assertEqual(modified, orig)
def test_basic(self):
threads = [
{"commentable_id": self.discussion1.discussion_id},
{"commentable_id": self.discussion2.discussion_id}
]
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"""
self.assertEqual(
set(thread.keys()),
set(["commentable_id", "courseware_url", "courseware_title"])
)
self.assertEqual(
thread.get("courseware_url"),
reverse(
"jump_to",
kwargs={
"course_id": self.course.id.to_deprecated_string(),
"location": discussion.location.to_deprecated_string()
}
)
)
self.assertEqual(thread.get("courseware_title"), expected_title)
assertThreadCorrect(threads[0], self.discussion1, "Chapter / Discussion 1")
assertThreadCorrect(threads[1], self.discussion2, "Subsection / Discussion 2")
def test_empty_discussion_subcategory_title(self):
"""
Test that for empty subcategory inline discussion modules,
the divider " / " is not rendered on a post or inline discussion topic label.
"""
discussion = ItemFactory.create(
parent_location=self.course.location,
category="discussion",
discussion_id="discussion",
discussion_category="Chapter",
discussion_target="" # discussion-subcategory
)
thread = {"commentable_id": discussion.discussion_id}
utils.add_courseware_context([thread], self.course, self.user)
self.assertNotIn('/', thread.get("courseware_title"))
@ddt.data((ModuleStoreEnum.Type.mongo, 2), (ModuleStoreEnum.Type.split, 1))
@ddt.unpack
def test_get_accessible_discussion_xblocks(self, modulestore_type, expected_discussion_xblocks):
"""
Tests that the accessible discussion xblocks having no parents do not get fetched for split modulestore.
"""
course = CourseFactory.create(default_store=modulestore_type)
# Create a discussion xblock.
test_discussion = self.store.create_child(self.user.id, course.location, 'discussion', 'test_discussion')
# Assert that created discussion xblock is not an orphan.
self.assertNotIn(test_discussion.location, self.store.get_orphans(course.id))
# Assert that there is only one discussion xblock in the course at the moment.
self.assertEqual(len(utils.get_accessible_discussion_xblocks(course, self.user)), 1)
# Add an orphan discussion xblock to that course
orphan = course.id.make_usage_key('discussion', 'orphan_discussion')
self.store.create_item(self.user.id, orphan.course_key, orphan.block_type, block_id=orphan.block_id)
# Assert that the discussion xblock is an orphan.
self.assertIn(orphan, self.store.get_orphans(course.id))
self.assertEqual(len(utils.get_accessible_discussion_xblocks(course, self.user)), expected_discussion_xblocks)
@attr(shard=3)
class CachedDiscussionIdMapTestCase(ModuleStoreTestCase):
"""
Tests that using the cache of discussion id mappings has the same behavior as searching through the course.
"""
ENABLED_SIGNALS = ['course_published']
def setUp(self):
super(CachedDiscussionIdMapTestCase, self).setUp()
self.course = CourseFactory.create(org='TestX', number='101', display_name='Test Course')
self.discussion = ItemFactory.create(
parent_location=self.course.location,
category='discussion',
discussion_id='test_discussion_id',
discussion_category='Chapter',
discussion_target='Discussion 1'
)
self.discussion2 = ItemFactory.create(
parent_location=self.course.location,
category='discussion',
discussion_id='test_discussion_id_2',
discussion_category='Chapter 2',
discussion_target='Discussion 2'
)
self.private_discussion = ItemFactory.create(
parent_location=self.course.location,
category='discussion',
discussion_id='private_discussion_id',
discussion_category='Chapter 3',
discussion_target='Beta Testing',
visible_to_staff_only=True
)
self.bad_discussion = ItemFactory.create(
parent_location=self.course.location,
category='discussion',
discussion_id='bad_discussion_id',
discussion_category=None,
discussion_target=None
)
def test_cache_returns_correct_key(self):
usage_key = utils.get_cached_discussion_key(self.course.id, 'test_discussion_id')
self.assertEqual(usage_key, self.discussion.location)
def test_cache_returns_none_if_id_is_not_present(self):
usage_key = utils.get_cached_discussion_key(self.course.id, 'bogus_id')
self.assertIsNone(usage_key)
def test_cache_raises_exception_if_course_structure_not_cached(self):
CourseStructure.objects.all().delete()
with self.assertRaises(utils.DiscussionIdMapIsNotCached):
utils.get_cached_discussion_key(self.course.id, 'test_discussion_id')
def test_cache_raises_exception_if_discussion_id_not_cached(self):
cache = CourseStructure.objects.get(course_id=self.course.id)
cache.discussion_id_map_json = None
cache.save()
with self.assertRaises(utils.DiscussionIdMapIsNotCached):
utils.get_cached_discussion_key(self.course.id, 'test_discussion_id')
def test_xblock_does_not_have_required_keys(self):
self.assertTrue(utils.has_required_keys(self.discussion))
self.assertFalse(utils.has_required_keys(self.bad_discussion))
def verify_discussion_metadata(self):
"""Retrieves the metadata for self.discussion and self.discussion2 and verifies that it is correct"""
metadata = utils.get_cached_discussion_id_map(
self.course,
['test_discussion_id', 'test_discussion_id_2'],
self.user
)
discussion1 = metadata[self.discussion.discussion_id]
discussion2 = metadata[self.discussion2.discussion_id]
self.assertEqual(discussion1['location'], self.discussion.location)
self.assertEqual(discussion1['title'], 'Chapter / Discussion 1')
self.assertEqual(discussion2['location'], self.discussion2.location)
self.assertEqual(discussion2['title'], 'Chapter 2 / Discussion 2')
def test_get_discussion_id_map_from_cache(self):
self.verify_discussion_metadata()
def test_get_discussion_id_map_without_cache(self):
CourseStructure.objects.all().delete()
self.verify_discussion_metadata()
def test_get_missing_discussion_id_map_from_cache(self):
metadata = utils.get_cached_discussion_id_map(self.course, ['bogus_id'], self.user)
self.assertEqual(metadata, {})
def test_get_discussion_id_map_from_cache_without_access(self):
user = UserFactory.create()
metadata = utils.get_cached_discussion_id_map(self.course, ['private_discussion_id'], self.user)
self.assertEqual(metadata['private_discussion_id']['title'], 'Chapter 3 / Beta Testing')
metadata = utils.get_cached_discussion_id_map(self.course, ['private_discussion_id'], user)
self.assertEqual(metadata, {})
def test_get_bad_discussion_id(self):
metadata = utils.get_cached_discussion_id_map(self.course, ['bad_discussion_id'], self.user)
self.assertEqual(metadata, {})
def test_discussion_id_accessible(self):
self.assertTrue(utils.discussion_category_id_access(self.course, self.user, 'test_discussion_id'))
def test_bad_discussion_id_not_accessible(self):
self.assertFalse(utils.discussion_category_id_access(self.course, self.user, 'bad_discussion_id'))
def test_missing_discussion_id_not_accessible(self):
self.assertFalse(utils.discussion_category_id_access(self.course, self.user, 'bogus_id'))
def test_discussion_id_not_accessible_without_access(self):
user = UserFactory.create()
self.assertTrue(utils.discussion_category_id_access(self.course, self.user, 'private_discussion_id'))
self.assertFalse(utils.discussion_category_id_access(self.course, user, 'private_discussion_id'))
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
)
@attr(shard=1)
class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
"""
Base testcase class for discussion categories for the
comment client service integration
"""
def setUp(self):
super(CategoryMapTestCase, self).setUp()
self.course = CourseFactory.create(
org="TestX", number="101", display_name="Test Course",
# 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.datetime(2012, 2, 3, tzinfo=UTC)
)
# Courses get a default discussion topic on creation, so remove it
self.course.discussion_topics = {}
self.discussion_num = 0
self.instructor = InstructorFactory(course_key=self.course.id)
self.maxDiff = None # pylint: disable=invalid-name
self.later = datetime.datetime(2050, 1, 1, tzinfo=UTC)
def create_discussion(self, discussion_category, discussion_target, **kwargs):
self.discussion_num += 1
return ItemFactory.create(
parent_location=self.course.location,
category="discussion",
discussion_id="discussion{}".format(self.discussion_num),
discussion_category=discussion_category,
discussion_target=discussion_target,
**kwargs
)
def assert_category_map_equals(self, expected, divided_only_if_explicit=False, exclude_unstarted=True): # pylint: disable=arguments-differ
"""
Asserts the expected map with the map returned by get_discussion_category_map method.
"""
self.assertEqual(
utils.get_discussion_category_map(
self.course, self.instructor, divided_only_if_explicit, exclude_unstarted
),
expected
)
def test_empty(self):
self.assert_category_map_equals({"entries": {}, "subcategories": {}, "children": []})
def test_configured_topics(self):
self.course.discussion_topics = {
"Topic A": {"id": "Topic_A"},
"Topic B": {"id": "Topic_B"},
"Topic C": {"id": "Topic_C"}
}
def check_cohorted_topics(expected_ids): # pylint: disable=missing-docstring
self.assert_category_map_equals(
{
"entries": {
"Topic A": {"id": "Topic_A", "sort_key": "Topic A", "is_divided": "Topic_A" in expected_ids},
"Topic B": {"id": "Topic_B", "sort_key": "Topic B", "is_divided": "Topic_B" in expected_ids},
"Topic C": {"id": "Topic_C", "sort_key": "Topic C", "is_divided": "Topic_C" in expected_ids},
},
"subcategories": {},
"children": [("Topic A", TYPE_ENTRY), ("Topic B", TYPE_ENTRY), ("Topic C", TYPE_ENTRY)]
}
)
check_cohorted_topics([]) # default (empty) cohort config
set_discussion_division_settings(self.course.id, enable_cohorts=False)
check_cohorted_topics([])
set_discussion_division_settings(self.course.id, enable_cohorts=True)
check_cohorted_topics([])
set_discussion_division_settings(
self.course.id,
enable_cohorts=True,
divided_discussions=["Topic_B", "Topic_C"]
)
check_cohorted_topics(["Topic_B", "Topic_C"])
set_discussion_division_settings(
self.course.id,
enable_cohorts=True,
divided_discussions=["Topic_A", "Some_Other_Topic"]
)
check_cohorted_topics(["Topic_A"])
# unlikely case, but make sure it works.
set_discussion_division_settings(
self.course.id,
enable_cohorts=False,
divided_discussions=["Topic_A"]
)
check_cohorted_topics([])
def test_single_inline(self):
self.create_discussion("Chapter", "Discussion")
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
"Chapter": {
"entries": {
"Discussion": {
"id": "discussion1",
"sort_key": None,
"is_divided": False,
}
},
"subcategories": {},
"children": [("Discussion", TYPE_ENTRY)]
}
},
"children": [("Chapter", TYPE_SUBCATEGORY)]
}
)
def test_inline_with_always_divide_inline_discussion_flag(self):
self.create_discussion("Chapter", "Discussion")
set_discussion_division_settings(self.course.id, enable_cohorts=True, always_divide_inline_discussions=True)
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
"Chapter": {
"entries": {
"Discussion": {
"id": "discussion1",
"sort_key": None,
"is_divided": True,
}
},
"subcategories": {},
"children": [("Discussion", TYPE_ENTRY)]
}
},
"children": [("Chapter", TYPE_SUBCATEGORY)]
}
)
def test_inline_without_always_divide_inline_discussion_flag(self):
self.create_discussion("Chapter", "Discussion")
set_discussion_division_settings(self.course.id, enable_cohorts=True)
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
"Chapter": {
"entries": {
"Discussion": {
"id": "discussion1",
"sort_key": None,
"is_divided": False,
}
},
"subcategories": {},
"children": [("Discussion", TYPE_ENTRY)]
}
},
"children": [("Chapter", TYPE_SUBCATEGORY)]
},
divided_only_if_explicit=True
)
def test_get_unstarted_discussion_xblocks(self):
self.create_discussion("Chapter 1", "Discussion 1", start=self.later)
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
"Chapter 1": {
"entries": {
"Discussion 1": {
"id": "discussion1",
"sort_key": None,
"is_divided": False,
"start_date": self.later
}
},
"subcategories": {},
"children": [("Discussion 1", TYPE_ENTRY)],
"start_date": self.later,
"sort_key": "Chapter 1"
}
},
"children": [("Chapter 1", TYPE_SUBCATEGORY)]
},
divided_only_if_explicit=True,
exclude_unstarted=False
)
def test_tree(self):
self.create_discussion("Chapter 1", "Discussion 1")
self.create_discussion("Chapter 1", "Discussion 2")
self.create_discussion("Chapter 2", "Discussion")
self.create_discussion("Chapter 2 / Section 1 / Subsection 1", "Discussion")
self.create_discussion("Chapter 2 / Section 1 / Subsection 2", "Discussion")
self.create_discussion("Chapter 3 / Section 1", "Discussion")
def check_divided(is_divided):
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
"Chapter 1": {
"entries": {
"Discussion 1": {
"id": "discussion1",
"sort_key": None,
"is_divided": is_divided,
},
"Discussion 2": {
"id": "discussion2",
"sort_key": None,
"is_divided": is_divided,
}
},
"subcategories": {},
"children": [("Discussion 1", TYPE_ENTRY), ("Discussion 2", TYPE_ENTRY)]
},
"Chapter 2": {
"entries": {
"Discussion": {
"id": "discussion3",
"sort_key": None,
"is_divided": is_divided,
}
},
"subcategories": {
"Section 1": {
"entries": {},
"subcategories": {
"Subsection 1": {
"entries": {
"Discussion": {
"id": "discussion4",
"sort_key": None,
"is_divided": is_divided,
}
},
"subcategories": {},
"children": [("Discussion", TYPE_ENTRY)]
},
"Subsection 2": {
"entries": {
"Discussion": {
"id": "discussion5",
"sort_key": None,
"is_divided": is_divided,
}
},
"subcategories": {},
"children": [("Discussion", TYPE_ENTRY)]
}
},
"children": [("Subsection 1", TYPE_SUBCATEGORY), ("Subsection 2", TYPE_SUBCATEGORY)]
}
},
"children": [("Discussion", TYPE_ENTRY), ("Section 1", TYPE_SUBCATEGORY)]
},
"Chapter 3": {
"entries": {},
"subcategories": {
"Section 1": {
"entries": {
"Discussion": {
"id": "discussion6",
"sort_key": None,
"is_divided": is_divided,
}
},
"subcategories": {},
"children": [("Discussion", TYPE_ENTRY)]
}
},
"children": [("Section 1", TYPE_SUBCATEGORY)]
}
},
"children": [("Chapter 1", TYPE_SUBCATEGORY), ("Chapter 2", TYPE_SUBCATEGORY),
("Chapter 3", TYPE_SUBCATEGORY)]
}
)
# empty / default config
check_divided(False)
# explicitly disabled cohorting
set_discussion_division_settings(self.course.id, enable_cohorts=False)
check_divided(False)
# explicitly enable courses divided by Cohort with inline discusssions also divided.
set_discussion_division_settings(self.course.id, enable_cohorts=True, always_divide_inline_discussions=True)
check_divided(True)
def test_tree_with_duplicate_targets(self):
self.create_discussion("Chapter 1", "Discussion A")
self.create_discussion("Chapter 1", "Discussion B")
self.create_discussion("Chapter 1", "Discussion A") # duplicate
self.create_discussion("Chapter 1", "Discussion A") # another duplicate
self.create_discussion("Chapter 2 / Section 1 / Subsection 1", "Discussion")
self.create_discussion("Chapter 2 / Section 1 / Subsection 1", "Discussion") # duplicate
category_map = utils.get_discussion_category_map(self.course, self.user)
chapter1 = category_map["subcategories"]["Chapter 1"]
chapter1_discussions = set(["Discussion A", "Discussion B", "Discussion A (1)", "Discussion A (2)"])
chapter1_discussions_with_types = set([("Discussion A", TYPE_ENTRY), ("Discussion B", TYPE_ENTRY),
("Discussion A (1)", TYPE_ENTRY), ("Discussion A (2)", TYPE_ENTRY)])
self.assertEqual(set(chapter1["children"]), chapter1_discussions_with_types)
self.assertEqual(set(chapter1["entries"].keys()), chapter1_discussions)
chapter2 = category_map["subcategories"]["Chapter 2"]
subsection1 = chapter2["subcategories"]["Section 1"]["subcategories"]["Subsection 1"]
subsection1_discussions = set(["Discussion", "Discussion (1)"])
subsection1_discussions_with_types = set([("Discussion", TYPE_ENTRY), ("Discussion (1)", TYPE_ENTRY)])
self.assertEqual(set(subsection1["children"]), subsection1_discussions_with_types)
self.assertEqual(set(subsection1["entries"].keys()), subsection1_discussions)
def test_start_date_filter(self):
now = datetime.datetime.now()
self.create_discussion("Chapter 1", "Discussion 1", start=now)
self.create_discussion("Chapter 1", "Discussion 2 обсуждение", start=self.later)
self.create_discussion("Chapter 2", "Discussion", start=now)
self.create_discussion("Chapter 2 / Section 1 / Subsection 1", "Discussion", start=self.later)
self.create_discussion("Chapter 2 / Section 1 / Subsection 2", "Discussion", start=self.later)
self.create_discussion("Chapter 3 / Section 1", "Discussion", start=self.later)
self.assertFalse(self.course.self_paced)
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
"Chapter 1": {
"entries": {
"Discussion 1": {
"id": "discussion1",
"sort_key": None,
"is_divided": False,
}
},
"subcategories": {},
"children": [("Discussion 1", TYPE_ENTRY)]
},
"Chapter 2": {
"entries": {
"Discussion": {
"id": "discussion3",
"sort_key": None,
"is_divided": False,
}
},
"subcategories": {},
"children": [("Discussion", TYPE_ENTRY)]
}
},
"children": [("Chapter 1", TYPE_SUBCATEGORY), ("Chapter 2", TYPE_SUBCATEGORY)]
}
)
def test_self_paced_start_date_filter(self):
self.course.self_paced = True
now = datetime.datetime.now()
self.create_discussion("Chapter 1", "Discussion 1", start=now)
self.create_discussion("Chapter 1", "Discussion 2", start=self.later)
self.create_discussion("Chapter 2", "Discussion", start=now)
self.create_discussion("Chapter 2 / Section 1 / Subsection 1", "Discussion", start=self.later)
self.create_discussion("Chapter 2 / Section 1 / Subsection 2", "Discussion", start=self.later)
self.create_discussion("Chapter 3 / Section 1", "Discussion", start=self.later)
self.assertTrue(self.course.self_paced)
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
"Chapter 1": {
"entries": {
"Discussion 1": {
"id": "discussion1",
"sort_key": None,
"is_divided": False,
},
"Discussion 2": {
"id": "discussion2",
"sort_key": None,
"is_divided": False,
}
},
"subcategories": {},
"children": [("Discussion 1", TYPE_ENTRY), ("Discussion 2", TYPE_ENTRY)]
},
"Chapter 2": {
"entries": {
"Discussion": {
"id": "discussion3",
"sort_key": None,
"is_divided": False,
}
},
"subcategories": {
"Section 1": {
"entries": {},
"subcategories": {
"Subsection 1": {
"entries": {
"Discussion": {
"id": "discussion4",
"sort_key": None,
"is_divided": False,
}
},
"subcategories": {},
"children": [("Discussion", TYPE_ENTRY)]
},
"Subsection 2": {
"entries": {
"Discussion": {
"id": "discussion5",
"sort_key": None,
"is_divided": False,
}
},
"subcategories": {},
"children": [("Discussion", TYPE_ENTRY)]
}
},
"children": [("Subsection 1", TYPE_SUBCATEGORY), ("Subsection 2", TYPE_SUBCATEGORY)]
}
},
"children": [("Discussion", TYPE_ENTRY), ("Section 1", TYPE_SUBCATEGORY)]
},
"Chapter 3": {
"entries": {},
"subcategories": {
"Section 1": {
"entries": {
"Discussion": {
"id": "discussion6",
"sort_key": None,
"is_divided": False,
}
},
"subcategories": {},
"children": [("Discussion", TYPE_ENTRY)]
}
},
"children": [("Section 1", TYPE_SUBCATEGORY)]
}
},
"children": [("Chapter 1", TYPE_SUBCATEGORY), ("Chapter 2", TYPE_SUBCATEGORY),
("Chapter 3", TYPE_SUBCATEGORY)]
}
)
def test_sort_inline_explicit(self):
self.create_discussion("Chapter", "Discussion 1", sort_key="D")
self.create_discussion("Chapter", "Discussion 2", sort_key="A")
self.create_discussion("Chapter", "Discussion 3", sort_key="E")
self.create_discussion("Chapter", "Discussion 4", sort_key="C")
self.create_discussion("Chapter", "Discussion 5", sort_key="B")
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
"Chapter": {
"entries": {
"Discussion 1": {
"id": "discussion1",
"sort_key": "D",
"is_divided": False,
},
"Discussion 2": {
"id": "discussion2",
"sort_key": "A",
"is_divided": False,
},
"Discussion 3": {
"id": "discussion3",
"sort_key": "E",
"is_divided": False,
},
"Discussion 4": {
"id": "discussion4",
"sort_key": "C",
"is_divided": False,
},
"Discussion 5": {
"id": "discussion5",
"sort_key": "B",
"is_divided": False,
}
},
"subcategories": {},
"children": [
("Discussion 2", TYPE_ENTRY),
("Discussion 5", TYPE_ENTRY),
("Discussion 4", TYPE_ENTRY),
("Discussion 1", TYPE_ENTRY),
("Discussion 3", TYPE_ENTRY)
]
}
},
"children": [("Chapter", TYPE_SUBCATEGORY)]
}
)
def test_sort_configured_topics_explicit(self):
self.course.discussion_topics = {
"Topic A": {"id": "Topic_A", "sort_key": "B"},
"Topic B": {"id": "Topic_B", "sort_key": "C"},
"Topic C": {"id": "Topic_C", "sort_key": "A"}
}
self.assert_category_map_equals(
{
"entries": {
"Topic A": {"id": "Topic_A", "sort_key": "B", "is_divided": False},
"Topic B": {"id": "Topic_B", "sort_key": "C", "is_divided": False},
"Topic C": {"id": "Topic_C", "sort_key": "A", "is_divided": False},
},
"subcategories": {},
"children": [("Topic C", TYPE_ENTRY), ("Topic A", TYPE_ENTRY), ("Topic B", TYPE_ENTRY)]
}
)
def test_sort_alpha(self):
self.course.discussion_sort_alpha = True
self.create_discussion("Chapter", "Discussion D")
self.create_discussion("Chapter", "Discussion A")
self.create_discussion("Chapter", "Discussion E")
self.create_discussion("Chapter", "Discussion C")
self.create_discussion("Chapter", "Discussion B")
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
"Chapter": {
"entries": {
"Discussion D": {
"id": "discussion1",
"sort_key": "Discussion D",
"is_divided": False,
},
"Discussion A": {
"id": "discussion2",
"sort_key": "Discussion A",
"is_divided": False,
},
"Discussion E": {
"id": "discussion3",
"sort_key": "Discussion E",
"is_divided": False,
},
"Discussion C": {
"id": "discussion4",
"sort_key": "Discussion C",
"is_divided": False,
},
"Discussion B": {
"id": "discussion5",
"sort_key": "Discussion B",
"is_divided": False,
}
},
"subcategories": {},
"children": [
("Discussion A", TYPE_ENTRY),
("Discussion B", TYPE_ENTRY),
("Discussion C", TYPE_ENTRY),
("Discussion D", TYPE_ENTRY),
("Discussion E", TYPE_ENTRY)
]
}
},
"children": [("Chapter", TYPE_SUBCATEGORY)]
}
)
def test_sort_intermediates(self):
self.create_discussion("Chapter B", "Discussion 2")
self.create_discussion("Chapter C", "Discussion")
self.create_discussion("Chapter A", "Discussion 1")
self.create_discussion("Chapter B", "Discussion 1")
self.create_discussion("Chapter A", "Discussion 2")
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
"Chapter A": {
"entries": {
"Discussion 1": {
"id": "discussion3",
"sort_key": None,
"is_divided": False,
},
"Discussion 2": {
"id": "discussion5",
"sort_key": None,
"is_divided": False,
}
},
"subcategories": {},
"children": [("Discussion 1", TYPE_ENTRY), ("Discussion 2", TYPE_ENTRY)]
},
"Chapter B": {
"entries": {
"Discussion 1": {
"id": "discussion4",
"sort_key": None,
"is_divided": False,
},
"Discussion 2": {
"id": "discussion1",
"sort_key": None,
"is_divided": False,
}
},
"subcategories": {},
"children": [("Discussion 1", TYPE_ENTRY), ("Discussion 2", TYPE_ENTRY)]
},
"Chapter C": {
"entries": {
"Discussion": {
"id": "discussion2",
"sort_key": None,
"is_divided": False,
}
},
"subcategories": {},
"children": [("Discussion", TYPE_ENTRY)]
}
},
"children": [("Chapter A", TYPE_SUBCATEGORY), ("Chapter B", TYPE_SUBCATEGORY),
("Chapter C", TYPE_SUBCATEGORY)]
}
)
def test_ids_empty(self):
self.assertEqual(utils.get_discussion_categories_ids(self.course, self.user), [])
def test_ids_configured_topics(self):
self.course.discussion_topics = {
"Topic A": {"id": "Topic_A"},
"Topic B": {"id": "Topic_B"},
"Topic C": {"id": "Topic_C"}
}
self.assertItemsEqual(
utils.get_discussion_categories_ids(self.course, self.user),
["Topic_A", "Topic_B", "Topic_C"]
)
def test_ids_inline(self):
self.create_discussion("Chapter 1", "Discussion 1")
self.create_discussion("Chapter 1", "Discussion 2")
self.create_discussion("Chapter 2", "Discussion")
self.create_discussion("Chapter 2 / Section 1 / Subsection 1", "Discussion")
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, self.user),
["discussion1", "discussion2", "discussion3", "discussion4", "discussion5", "discussion6"]
)
def test_ids_mixed(self):
self.course.discussion_topics = {
"Topic A": {"id": "Topic_A"},
"Topic B": {"id": "Topic_B"},
"Topic C": {"id": "Topic_C"}
}
self.create_discussion("Chapter 1", "Discussion 1")
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, self.user),
["Topic_A", "Topic_B", "Topic_C", "discussion1", "discussion2", "discussion3"]
)
@attr(shard=1)
class ContentGroupCategoryMapTestCase(CategoryMapTestMixin, ContentGroupTestCase):
"""
Tests `get_discussion_category_map` on discussion xblocks 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', 'entry'),
('Visible to Beta', 'entry'),
('Visible to Everyone', 'entry')
],
'entries': {
'Visible to Alpha': {
'sort_key': None,
'is_divided': False,
'id': 'alpha_group_discussion'
},
'Visible to Beta': {
'sort_key': None,
'is_divided': False,
'id': 'beta_group_discussion'
},
'Visible to Everyone': {
'sort_key': None,
'is_divided': False,
'id': 'global_group_discussion'
}
}
}
},
'children': [('General', 'entry'), ('Week 1', 'subcategory')],
'entries': {
'General': {
'sort_key': 'General',
'is_divided': 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', 'entry'),
('Visible to Everyone', 'entry')
],
'entries': {
'Visible to Alpha': {
'sort_key': None,
'is_divided': False,
'id': 'alpha_group_discussion'
},
'Visible to Everyone': {
'sort_key': None,
'is_divided': False,
'id': 'global_group_discussion'
}
}
}
},
'children': [('General', 'entry'), ('Week 1', 'subcategory')],
'entries': {
'General': {
'sort_key': 'General',
'is_divided': 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', 'entry'),
('Visible to Everyone', 'entry')
],
'entries': {
'Visible to Beta': {
'sort_key': None,
'is_divided': False,
'id': 'beta_group_discussion'
},
'Visible to Everyone': {
'sort_key': None,
'is_divided': False,
'id': 'global_group_discussion'
}
}
}
},
'children': [('General', 'entry'), ('Week 1', 'subcategory')],
'entries': {
'General': {
'sort_key': 'General',
'is_divided': 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', 'entry')
],
'entries': {
'Visible to Everyone': {
'sort_key': None,
'is_divided': False,
'id': 'global_group_discussion'
}
}
}
},
'children': [('General', 'entry'), ('Week 1', 'subcategory')],
'entries': {
'General': {
'sort_key': 'General',
'is_divided': 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)
reparsed = json.loads(response.content)
self.assertEqual(reparsed, text)
class DiscussionTabTestCase(ModuleStoreTestCase):
""" Test visibility of the discussion tab. """
def setUp(self):
super(DiscussionTabTestCase, self).setUp()
self.course = CourseFactory.create()
self.enrolled_user = UserFactory.create()
self.staff_user = AdminFactory.create()
CourseEnrollmentFactory.create(user=self.enrolled_user, course_id=self.course.id)
self.unenrolled_user = UserFactory.create()
def discussion_tab_present(self, user):
""" Returns true if the user has access to the discussion tab. """
request = RequestFactory().request()
request.user = user
all_tabs = get_course_tab_list(request, self.course)
return any(tab.type == 'discussion' for tab in all_tabs)
def test_tab_access(self):
with self.settings(FEATURES={'ENABLE_DISCUSSION_SERVICE': True}):
self.assertTrue(self.discussion_tab_present(self.staff_user))
self.assertTrue(self.discussion_tab_present(self.enrolled_user))
self.assertFalse(self.discussion_tab_present(self.unenrolled_user))
@mock.patch('ccx.overrides.get_current_ccx')
def test_tab_settings(self, mock_get_ccx):
mock_get_ccx.return_value = True
with self.settings(FEATURES={'ENABLE_DISCUSSION_SERVICE': False}):
self.assertFalse(self.discussion_tab_present(self.enrolled_user))
with self.settings(FEATURES={'CUSTOM_COURSES_EDX': True}):
self.assertFalse(self.discussion_tab_present(self.enrolled_user))
class IsCommentableDividedTestCase(ModuleStoreTestCase):
"""
Test the is_commentable_divided function.
"""
MODULESTORE = TEST_DATA_MIXED_MODULESTORE
def setUp(self):
"""
Make sure that course is reloaded every time--clear out the modulestore.
"""
super(IsCommentableDividedTestCase, self).setUp()
self.toy_course_key = ToyCourseFactory.create().id
def test_is_commentable_divided(self):
course = modulestore().get_course(self.toy_course_key)
self.assertFalse(cohorts.is_course_cohorted(course.id))
def to_id(name):
"""Helper for topic_name_to_id that uses course."""
return topic_name_to_id(course, name)
# no topics
self.assertFalse(
utils.is_commentable_divided(course.id, to_id("General")),
"Course doesn't even have a 'General' topic"
)
# not cohorted
config_course_cohorts(course, is_cohorted=False)
config_course_discussions(course, discussion_topics=["General", "Feedback"])
self.assertFalse(
utils.is_commentable_divided(course.id, to_id("General")),
"Course isn't cohorted"
)
# cohorted, but top level topics aren't
config_course_cohorts(course, is_cohorted=True)
config_course_discussions(course, discussion_topics=["General", "Feedback"])
self.assertTrue(cohorts.is_course_cohorted(course.id))
self.assertFalse(
utils.is_commentable_divided(course.id, to_id("General")),
"Course is cohorted, but 'General' isn't."
)
# cohorted, including "Feedback" top-level topics aren't
config_course_cohorts(
course,
is_cohorted=True
)
config_course_discussions(course, discussion_topics=["General", "Feedback"], divided_discussions=["Feedback"])
self.assertTrue(cohorts.is_course_cohorted(course.id))
self.assertFalse(
utils.is_commentable_divided(course.id, to_id("General")),
"Course is cohorted, but 'General' isn't."
)
self.assertTrue(
utils.is_commentable_divided(course.id, to_id("Feedback")),
"Feedback was listed as cohorted. Should be."
)
def test_is_commentable_divided_inline_discussion(self):
course = modulestore().get_course(self.toy_course_key)
self.assertFalse(cohorts.is_course_cohorted(course.id))
def to_id(name): # pylint: disable=missing-docstring
return topic_name_to_id(course, name)
config_course_cohorts(
course,
is_cohorted=True,
)
config_course_discussions(
course,
discussion_topics=["General", "Feedback"],
divided_discussions=["Feedback", "random_inline"]
)
self.assertFalse(
utils.is_commentable_divided(course.id, to_id("random")),
"By default, Non-top-level discussions are not cohorted in a cohorted courses."
)
# if always_divide_inline_discussions is set to False, non-top-level discussion are always
# not divided unless they are explicitly set in divided_discussions
config_course_cohorts(
course,
is_cohorted=True,
)
config_course_discussions(
course,
discussion_topics=["General", "Feedback"],
divided_discussions=["Feedback", "random_inline"],
always_divide_inline_discussions=False
)
self.assertFalse(
utils.is_commentable_divided(course.id, to_id("random")),
"Non-top-level discussion is not cohorted if always_divide_inline_discussions is False."
)
self.assertTrue(
utils.is_commentable_divided(course.id, to_id("random_inline")),
"If always_divide_inline_discussions set to False, Non-top-level discussion is "
"cohorted if explicitly set in cohorted_discussions."
)
self.assertTrue(
utils.is_commentable_divided(course.id, to_id("Feedback")),
"If always_divide_inline_discussions set to False, top-level discussion are not affected."
)
def test_is_commentable_divided_team(self):
course = modulestore().get_course(self.toy_course_key)
self.assertFalse(cohorts.is_course_cohorted(course.id))
config_course_cohorts(course, is_cohorted=True)
config_course_discussions(course, always_divide_inline_discussions=True)
team = CourseTeamFactory(course_id=course.id)
# Verify that team discussions are not cohorted, but other discussions are
# if "always cohort inline discussions" is set to true.
self.assertFalse(utils.is_commentable_divided(course.id, team.discussion_topic_id))
self.assertTrue(utils.is_commentable_divided(course.id, "random"))
def test_is_commentable_divided_cohorts(self):
course = modulestore().get_course(self.toy_course_key)
set_discussion_division_settings(
course.id,
enable_cohorts=True,
divided_discussions=[],
always_divide_inline_discussions=True,
division_scheme=CourseDiscussionSettings.NONE,
)
# Although Cohorts are enabled, discussion division is explicitly disabled.
self.assertFalse(utils.is_commentable_divided(course.id, "random"))
# Now set the discussion division scheme.
set_discussion_division_settings(
course.id,
enable_cohorts=True,
divided_discussions=[],
always_divide_inline_discussions=True,
division_scheme=CourseDiscussionSettings.COHORT,
)
self.assertTrue(utils.is_commentable_divided(course.id, "random"))
def test_is_commentable_divided_enrollment_track(self):
course = modulestore().get_course(self.toy_course_key)
set_discussion_division_settings(
course.id,
divided_discussions=[],
always_divide_inline_discussions=True,
division_scheme=CourseDiscussionSettings.ENROLLMENT_TRACK,
)
# Although division scheme is set to ENROLLMENT_TRACK, divided returns
# False because there is only a single enrollment mode.
self.assertFalse(utils.is_commentable_divided(course.id, "random"))
# Now create 2 explicit course modes.
CourseModeFactory.create(course_id=course.id, mode_slug=CourseMode.AUDIT)
CourseModeFactory.create(course_id=course.id, mode_slug=CourseMode.VERIFIED)
self.assertTrue(utils.is_commentable_divided(course.id, "random"))
@attr(shard=1)
class GroupIdForUserTestCase(ModuleStoreTestCase):
""" Test the get_group_id_for_user method. """
def setUp(self):
super(GroupIdForUserTestCase, self).setUp()
self.course = CourseFactory.create()
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.AUDIT)
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
self.test_user = UserFactory.create()
CourseEnrollmentFactory.create(
mode=CourseMode.VERIFIED, user=self.test_user, course_id=self.course.id
)
self.test_cohort = CohortFactory(
course_id=self.course.id,
name='Test Cohort',
users=[self.test_user]
)
def test_discussion_division_disabled(self):
course_discussion_settings = get_course_discussion_settings(self.course.id)
self.assertEqual(CourseDiscussionSettings.NONE, course_discussion_settings.division_scheme)
self.assertIsNone(utils.get_group_id_for_user(self.test_user, course_discussion_settings))
def test_discussion_division_by_cohort(self):
set_discussion_division_settings(
self.course.id, enable_cohorts=True, division_scheme=CourseDiscussionSettings.COHORT
)
course_discussion_settings = get_course_discussion_settings(self.course.id)
self.assertEqual(CourseDiscussionSettings.COHORT, course_discussion_settings.division_scheme)
self.assertEqual(
self.test_cohort.id,
utils.get_group_id_for_user(self.test_user, course_discussion_settings)
)
def test_discussion_division_by_enrollment_track(self):
set_discussion_division_settings(
self.course.id, division_scheme=CourseDiscussionSettings.ENROLLMENT_TRACK
)
course_discussion_settings = get_course_discussion_settings(self.course.id)
self.assertEqual(CourseDiscussionSettings.ENROLLMENT_TRACK, course_discussion_settings.division_scheme)
self.assertEqual(
-2, # Verified has group ID 2, and we negate that value to ensure unique IDs
utils.get_group_id_for_user(self.test_user, course_discussion_settings)
)
@attr(shard=1)
class CourseDiscussionDivisionEnabledTestCase(ModuleStoreTestCase):
""" Test the course_discussion_division_enabled and available_division_schemes methods. """
def setUp(self):
super(CourseDiscussionDivisionEnabledTestCase, self).setUp()
self.course = CourseFactory.create()
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.AUDIT)
self.test_cohort = CohortFactory(
course_id=self.course.id,
name='Test Cohort',
users=[]
)
def test_discussion_division_disabled(self):
course_discussion_settings = get_course_discussion_settings(self.course.id)
self.assertFalse(utils.course_discussion_division_enabled(course_discussion_settings))
self.assertEqual([], utils.available_division_schemes(self.course.id))
def test_discussion_division_by_cohort(self):
set_discussion_division_settings(
self.course.id, enable_cohorts=False, division_scheme=CourseDiscussionSettings.COHORT
)
# Because cohorts are disabled, discussion division is not enabled.
self.assertFalse(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id)))
self.assertEqual([], utils.available_division_schemes(self.course.id))
# Now enable cohorts, which will cause discussions to be divided.
set_discussion_division_settings(
self.course.id, enable_cohorts=True, division_scheme=CourseDiscussionSettings.COHORT
)
self.assertTrue(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id)))
self.assertEqual([CourseDiscussionSettings.COHORT], utils.available_division_schemes(self.course.id))
def test_discussion_division_by_enrollment_track(self):
set_discussion_division_settings(
self.course.id, division_scheme=CourseDiscussionSettings.ENROLLMENT_TRACK
)
# Only a single enrollment track exists, so discussion division is not enabled.
self.assertFalse(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id)))
self.assertEqual([], utils.available_division_schemes(self.course.id))
# Now create a second CourseMode, which will cause discussions to be divided.
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
self.assertTrue(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id)))
self.assertEqual([CourseDiscussionSettings.ENROLLMENT_TRACK], utils.available_division_schemes(self.course.id))
@attr(shard=1)
class GroupNameTestCase(ModuleStoreTestCase):
""" Test the get_group_name and get_group_names_by_id methods. """
def setUp(self):
super(GroupNameTestCase, self).setUp()
self.course = CourseFactory.create()
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.AUDIT)
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
self.test_cohort_1 = CohortFactory(
course_id=self.course.id,
name='Cohort 1',
users=[]
)
self.test_cohort_2 = CohortFactory(
course_id=self.course.id,
name='Cohort 2',
users=[]
)
def test_discussion_division_disabled(self):
course_discussion_settings = get_course_discussion_settings(self.course.id)
self.assertEqual({}, utils.get_group_names_by_id(course_discussion_settings))
self.assertIsNone(utils.get_group_name(-1000, course_discussion_settings))
def test_discussion_division_by_cohort(self):
set_discussion_division_settings(
self.course.id, enable_cohorts=True, division_scheme=CourseDiscussionSettings.COHORT
)
course_discussion_settings = get_course_discussion_settings(self.course.id)
self.assertEqual(
{
self.test_cohort_1.id: self.test_cohort_1.name,
self.test_cohort_2.id: self.test_cohort_2.name
},
utils.get_group_names_by_id(course_discussion_settings)
)
self.assertEqual(
self.test_cohort_2.name,
utils.get_group_name(self.test_cohort_2.id, course_discussion_settings)
)
# Test also with a group_id that doesn't exist.
self.assertIsNone(
utils.get_group_name(-1000, course_discussion_settings)
)
def test_discussion_division_by_enrollment_track(self):
set_discussion_division_settings(
self.course.id, division_scheme=CourseDiscussionSettings.ENROLLMENT_TRACK
)
course_discussion_settings = get_course_discussion_settings(self.course.id)
self.assertEqual(
{
-1: "audit course",
-2: "verified course"
},
utils.get_group_names_by_id(course_discussion_settings)
)
self.assertEqual(
"verified course",
utils.get_group_name(-2, course_discussion_settings)
)
# Test also with a group_id that doesn't exist.
self.assertIsNone(
utils.get_group_name(-1000, course_discussion_settings)
)
class PermissionsTestCase(ModuleStoreTestCase):
"""Test utils functionality related to forums "abilities" (permissions)"""
def test_get_ability(self):
content = {}
content['user_id'] = '1'
content['type'] = 'thread'
user = mock.Mock()
user.id = 1
with mock.patch('django_comment_client.utils.check_permissions_by_view') as check_perm:
check_perm.return_value = True
self.assertEqual(utils.get_ability(None, content, user), {
'editable': True,
'can_reply': True,
'can_delete': True,
'can_openclose': True,
'can_vote': False,
'can_report': False
})
content['user_id'] = '2'
self.assertEqual(utils.get_ability(None, content, user), {
'editable': True,
'can_reply': True,
'can_delete': True,
'can_openclose': True,
'can_vote': True,
'can_report': True
})
def test_get_ability_with_global_staff(self):
"""
Tests that global staff has rights to report other user's post inspite
of enrolled in the course or not.
"""
content = {'user_id': '1', 'type': 'thread'}
with mock.patch('django_comment_client.utils.check_permissions_by_view') as check_perm:
# check_permissions_by_view returns false because user is not enrolled in the course.
check_perm.return_value = False
global_staff = UserFactory(username='global_staff', email='global_staff@edx.org', is_staff=True)
self.assertEqual(utils.get_ability(None, content, global_staff), {
'editable': False,
'can_reply': False,
'can_delete': False,
'can_openclose': False,
'can_vote': False,
'can_report': True
})
def test_is_content_authored_by(self):
content = {}
user = mock.Mock()
user.id = 1
# strict equality checking
content['user_id'] = 1
self.assertTrue(utils.is_content_authored_by(content, user))
# cast from string to int
content['user_id'] = '1'
self.assertTrue(utils.is_content_authored_by(content, user))
# strict equality checking, fails
content['user_id'] = 2
self.assertFalse(utils.is_content_authored_by(content, user))
# cast from string to int, fails
content['user_id'] = 'string'
self.assertFalse(utils.is_content_authored_by(content, user))
# content has no known author
del content['user_id']
self.assertFalse(utils.is_content_authored_by(content, user))
class GroupModeratorPermissionsTestCase(ModuleStoreTestCase):
"""Test utils functionality related to forums "abilities" (permissions) for group moderators"""
def _check_condition(user, condition, content):
"""
Mocks check_condition method because is_open and is_team_member_if_applicable must always be true
in order to interact with a thread or comment.
"""
return True if condition == 'is_open' or condition == 'is_team_member_if_applicable' else False
def setUp(self):
super(GroupModeratorPermissionsTestCase, self).setUp()
# Create course, seed permissions roles, and create team
self.course = CourseFactory.create()
seed_permissions_roles(self.course.id)
verified_coursemode = CourseModeFactory.create(
course_id=self.course.id,
mode_slug=CourseMode.VERIFIED
)
audit_coursemode = CourseModeFactory.create(
course_id=self.course.id,
mode_slug=CourseMode.AUDIT
)
# Create four users: group_moderator (who is within the verified enrollment track and in the cohort),
# verified_user (who is in the verified enrollment track but not the cohort),
# cohorted_user (who is in the cohort but not the verified enrollment track),
# and plain_user (who is neither in the cohort nor the verified enrollment track)
self.group_moderator = UserFactory(username='group_moderator', email='group_moderator@edx.org')
CourseEnrollmentFactory(
course_id=self.course.id,
user=self.group_moderator,
mode=verified_coursemode
)
self.verified_user = UserFactory(username='verified', email='verified@edx.org')
CourseEnrollmentFactory(
course_id=self.course.id,
user=self.verified_user,
mode=verified_coursemode
)
self.cohorted_user = UserFactory(username='cohort', email='cohort@edx.org')
CourseEnrollmentFactory(
course_id=self.course.id,
user=self.cohorted_user,
mode=audit_coursemode
)
self.plain_user = UserFactory(username='plain', email='plain@edx.org')
CourseEnrollmentFactory(
course_id=self.course.id,
user=self.plain_user,
mode=audit_coursemode
)
CohortFactory(
course_id=self.course.id,
name='Test Cohort',
users=[self.group_moderator, self.cohorted_user]
)
# Give group moderator permissions to group_moderator
assign_role(self.course.id, self.group_moderator, 'Group Moderator')
@mock.patch('django_comment_client.permissions._check_condition', side_effect=_check_condition)
def test_not_divided(self, check_condition_function):
"""
Group moderator should not have moderator permissions if the discussions are not divided.
"""
content = {'user_id': self.plain_user.id, 'type': 'thread', 'username': self.plain_user.username}
self.assertEqual(utils.get_ability(self.course.id, content, self.group_moderator), {
'editable': False,
'can_reply': True,
'can_delete': False,
'can_openclose': False,
'can_vote': True,
'can_report': True
})
content = {'user_id': self.cohorted_user.id, 'type': 'thread'}
self.assertEqual(utils.get_ability(self.course.id, content, self.group_moderator), {
'editable': False,
'can_reply': True,
'can_delete': False,
'can_openclose': False,
'can_vote': True,
'can_report': True
})
content = {'user_id': self.verified_user.id, 'type': 'thread'}
self.assertEqual(utils.get_ability(self.course.id, content, self.group_moderator), {
'editable': False,
'can_reply': True,
'can_delete': False,
'can_openclose': False,
'can_vote': True,
'can_report': True
})
@mock.patch('django_comment_client.permissions._check_condition', side_effect=_check_condition)
def test_divided_within_group(self, check_condition_function):
"""
Group moderator should have moderator permissions within their group if the discussions are divided.
"""
set_discussion_division_settings(self.course.id, enable_cohorts=True,
division_scheme=CourseDiscussionSettings.COHORT)
content = {'user_id': self.cohorted_user.id, 'type': 'thread', 'username': self.cohorted_user.username}
self.assertEqual(utils.get_ability(self.course.id, content, self.group_moderator), {
'editable': True,
'can_reply': True,
'can_delete': True,
'can_openclose': True,
'can_vote': True,
'can_report': True
})
set_discussion_division_settings(self.course.id, division_scheme=CourseDiscussionSettings.ENROLLMENT_TRACK)
content = {'user_id': self.verified_user.id, 'type': 'thread', 'username': self.verified_user.username}
self.assertEqual(utils.get_ability(self.course.id, content, self.group_moderator), {
'editable': True,
'can_reply': True,
'can_delete': True,
'can_openclose': True,
'can_vote': True,
'can_report': True
})
@mock.patch('django_comment_client.permissions._check_condition', side_effect=_check_condition)
def test_divided_outside_group(self, check_condition_function):
"""
Group moderator should not have moderator permissions outside of their group.
"""
content = {'user_id': self.plain_user.id, 'type': 'thread', 'username': self.plain_user.username}
set_discussion_division_settings(self.course.id, division_scheme=CourseDiscussionSettings.NONE)
self.assertEqual(utils.get_ability(self.course.id, content, self.group_moderator), {
'editable': False,
'can_reply': True,
'can_delete': False,
'can_openclose': False,
'can_vote': True,
'can_report': True
})
class ClientConfigurationTestCase(TestCase):
"""Simple test cases to ensure enabling/disabling the use of the comment service works as intended."""
def test_disabled(self):
"""Ensures that an exception is raised when forums are disabled."""
config = ForumsConfig.current()
config.enabled = False
config.save()
with self.assertRaises(CommentClientMaintenanceError):
perform_request('GET', 'http://www.google.com')
@patch('requests.request')
def test_enabled(self, mock_request):
"""Ensures that requests proceed normally when forums are enabled."""
config = ForumsConfig.current()
config.enabled = True
config.save()
response = Mock()
response.status_code = 200
response.json = lambda: {}
mock_request.return_value = response
result = perform_request('GET', 'http://www.google.com')
self.assertEqual(result, {})
def set_discussion_division_settings(
course_key, enable_cohorts=False, always_divide_inline_discussions=False,
divided_discussions=[], division_scheme=CourseDiscussionSettings.COHORT
):
"""
Convenience method for setting cohort enablement and discussion settings.
COHORT is the default division_scheme, as no other schemes were supported at
the time that the unit tests were originally written.
"""
set_course_discussion_settings(
course_key=course_key,
divided_discussions=divided_discussions,
division_scheme=division_scheme,
always_divide_inline_discussions=always_divide_inline_discussions,
)
set_course_cohorted(course_key, enable_cohorts)