Merge pull request #15037 from edx/christina/ed11-simplification

Feature branch: dividing discussions by enrollment tracks
This commit is contained in:
Albert (AJ) St. Aubin
2017-06-07 13:22:13 -04:00
committed by GitHub
79 changed files with 3371 additions and 2013 deletions

View File

@@ -118,7 +118,31 @@ def is_course_cohorted(course_key):
Raises:
Http404 if the course doesn't exist.
"""
return get_course_cohort_settings(course_key).is_cohorted
return _get_course_cohort_settings(course_key).is_cohorted
def get_course_cohort_id(course_key):
"""
Given a course key, return the int id for the cohort settings.
Raises:
Http404 if the course doesn't exist.
"""
return _get_course_cohort_settings(course_key).id
def set_course_cohorted(course_key, cohorted):
"""
Given a course course and a boolean, sets whether or not the course is cohorted.
Raises:
Value error if `cohorted` is not a boolean
"""
if not isinstance(cohorted, bool):
raise ValueError("Cohorted must be a boolean")
course_cohort_settings = _get_course_cohort_settings(course_key)
course_cohort_settings.is_cohorted = cohorted
course_cohort_settings.save()
def get_cohort_id(user, course_key, use_cached=False):
@@ -130,22 +154,6 @@ def get_cohort_id(user, course_key, use_cached=False):
return None if cohort is None else cohort.id
def get_cohorted_commentables(course_key):
"""
Given a course_key return a set of strings representing cohorted commentables.
"""
course_cohort_settings = get_course_cohort_settings(course_key)
if not course_cohort_settings.is_cohorted:
# this is the easy case :)
ans = set()
else:
ans = set(course_cohort_settings.cohorted_discussions)
return ans
COHORT_CACHE_NAMESPACE = u"cohorts.get_cohort"
@@ -213,8 +221,7 @@ def get_cohort(user, course_key, assign=True, use_cached=False):
# First check whether the course is cohorted (users shouldn't be in a cohort
# in non-cohorted courses, but settings can change after course starts)
course_cohort_settings = get_course_cohort_settings(course_key)
if not course_cohort_settings.is_cohorted:
if not is_course_cohorted(course_key):
return cache.setdefault(cache_key, None)
# If course is cohorted, check if the user already has a cohort.
@@ -277,11 +284,7 @@ def migrate_cohort_settings(course):
"""
cohort_settings, created = CourseCohortsSettings.objects.get_or_create(
course_id=course.id,
defaults={
'is_cohorted': course.is_cohorted,
'cohorted_discussions': list(course.cohorted_discussions),
'always_cohort_inline_discussions': course.always_cohort_inline_discussions
}
defaults=_get_cohort_settings_from_modulestore(course)
)
# Add the new and update the existing cohorts
@@ -507,43 +510,20 @@ def is_last_random_cohort(user_group):
return len(random_cohorts) == 1 and random_cohorts[0].name == user_group.name
def set_course_cohort_settings(course_key, **kwargs):
"""
Set cohort settings for a course.
Arguments:
course_key: CourseKey
is_cohorted (bool): If the course should be cohorted.
always_cohort_inline_discussions (bool): If inline discussions should always be cohorted.
cohorted_discussions (list): List of discussion ids.
Returns:
A CourseCohortSettings object.
Raises:
Http404 if course_key is invalid.
"""
fields = {'is_cohorted': bool, 'always_cohort_inline_discussions': bool, 'cohorted_discussions': list}
course_cohort_settings = get_course_cohort_settings(course_key)
for field, field_type in fields.items():
if field in kwargs:
if not isinstance(kwargs[field], field_type):
raise ValueError("Incorrect field type for `{}`. Type must be `{}`".format(field, field_type.__name__))
setattr(course_cohort_settings, field, kwargs[field])
course_cohort_settings.save()
return course_cohort_settings
@request_cached
def get_course_cohort_settings(course_key):
def _get_course_cohort_settings(course_key):
"""
Return cohort settings for a course.
Return cohort settings for a course. NOTE that the only non-deprecated fields in
CourseCohortSettings are `course_id` and `is_cohorted`. Other fields should only be used for
migration purposes.
Arguments:
course_key: CourseKey
Returns:
A CourseCohortSettings object.
A CourseCohortSettings object. NOTE that the only non-deprecated field in
CourseCohortSettings are `course_id` and `is_cohorted`. Other fields should only be used
for migration purposes.
Raises:
Http404 if course_key is invalid.
@@ -554,3 +534,25 @@ def get_course_cohort_settings(course_key):
course = courses.get_course_by_id(course_key)
course_cohort_settings = migrate_cohort_settings(course)
return course_cohort_settings
def get_legacy_discussion_settings(course_key):
try:
course_cohort_settings = CourseCohortsSettings.objects.get(course_id=course_key)
return {
'is_cohorted': course_cohort_settings.is_cohorted,
'cohorted_discussions': course_cohort_settings.cohorted_discussions,
'always_cohort_inline_discussions': course_cohort_settings.always_cohort_inline_discussions
}
except CourseCohortsSettings.DoesNotExist:
course = courses.get_course_by_id(course_key)
return _get_cohort_settings_from_modulestore(course)
def _get_cohort_settings_from_modulestore(course):
return {
'is_cohorted': course.is_cohorted,
'cohorted_discussions': list(course.cohorted_discussions),
'always_cohort_inline_discussions': course.always_cohort_inline_discussions
}

View File

@@ -168,6 +168,7 @@ class CourseUserGroupPartitionGroup(models.Model):
class CourseCohortsSettings(models.Model):
"""
This model represents cohort settings for courses.
The only non-deprecated fields are `is_cohorted` and `course_id`.
"""
is_cohorted = models.BooleanField(default=False)
@@ -184,16 +185,23 @@ class CourseCohortsSettings(models.Model):
# in reality the default value at the time that cohorting is enabled for a course comes from
# course_module.always_cohort_inline_discussions (via `migrate_cohort_settings`).
# pylint: disable=invalid-name
# DEPRECATED-- DO NOT USE: Instead use `CourseDiscussionSettings.always_divide_inline_discussions`
# via `get_course_discussion_settings` or `set_course_discussion_settings`.
always_cohort_inline_discussions = models.BooleanField(default=False)
@property
def cohorted_discussions(self):
"""Jsonify the cohorted_discussions"""
"""
DEPRECATED-- DO NOT USE. Instead use `CourseDiscussionSettings.divided_discussions`
via `get_course_discussion_settings`.
"""
return json.loads(self._cohorted_discussions)
@cohorted_discussions.setter
def cohorted_discussions(self, value):
"""Un-Jsonify the cohorted_discussions"""
"""
DEPRECATED-- DO NOT USE. Instead use `CourseDiscussionSettings` via `set_course_discussion_settings`.
"""
self._cohorted_discussions = json.dumps(value)

View File

@@ -2,16 +2,18 @@
Helper methods for testing cohorts.
"""
from factory import post_generation, Sequence
from factory.django import DjangoModelFactory
import json
from django_comment_common.models import CourseDiscussionSettings
from django_comment_common.utils import set_course_discussion_settings
from factory import Sequence, post_generation
from factory.django import DjangoModelFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from ..cohorts import set_course_cohort_settings
from ..models import CourseUserGroup, CourseCohort, CourseCohortsSettings, CohortMembership
from ..cohorts import set_course_cohorted
from ..models import CohortMembership, CourseCohort, CourseCohortsSettings, CourseUserGroup
class CohortFactory(DjangoModelFactory):
@@ -61,25 +63,10 @@ class CourseCohortSettingsFactory(DjangoModelFactory):
always_cohort_inline_discussions = False
def topic_name_to_id(course, name):
"""
Given a discussion topic name, return an id for that name (includes
course and url_name).
"""
return "{course}_{run}_{name}".format(
course=course.location.course,
run=course.url_name,
name=name
)
def config_course_cohorts_legacy(
course,
discussions,
cohorted,
cohorted_discussions=None,
auto_cohort_groups=None,
always_cohort_inline_discussions=None
auto_cohort_groups=None
):
"""
Given a course with no discussion set up, add the discussions and set
@@ -91,39 +78,19 @@ def config_course_cohorts_legacy(
Arguments:
course: CourseDescriptor
discussions: list of topic names strings. Picks ids and sort_keys
automatically.
cohorted: bool.
cohorted_discussions: optional list of topic names. If specified,
converts them to use the same ids as topic names.
auto_cohort_groups: optional list of strings
(names of groups to put students into).
Returns:
Nothing -- modifies course in place.
"""
def to_id(name):
"""
Helper method to convert a discussion topic name to a database identifier
"""
return topic_name_to_id(course, name)
topics = dict((name, {"sort_key": "A",
"id": to_id(name)})
for name in discussions)
course.discussion_topics = topics
course.discussion_topics = {}
config = {"cohorted": cohorted}
if cohorted_discussions is not None:
config["cohorted_discussions"] = [to_id(name)
for name in cohorted_discussions]
if auto_cohort_groups is not None:
config["auto_cohort_groups"] = auto_cohort_groups
if always_cohort_inline_discussions is not None:
config["always_cohort_inline_discussions"] = always_cohort_inline_discussions
course.cohort_config = config
try:
@@ -137,39 +104,29 @@ def config_course_cohorts_legacy(
def config_course_cohorts(
course,
is_cohorted,
discussion_division_scheme=CourseDiscussionSettings.COHORT,
auto_cohorts=[],
manual_cohorts=[],
discussion_topics=[],
cohorted_discussions=[],
always_cohort_inline_discussions=False
):
"""
Set discussions and configure cohorts for a course.
Set and configure cohorts for a course.
Arguments:
course: CourseDescriptor
is_cohorted (bool): Is the course cohorted?
discussion_division_scheme (String): the division scheme for discussions. Default is
CourseDiscussionSettings.COHORT.
auto_cohorts (list): Names of auto cohorts to create.
manual_cohorts (list): Names of manual cohorts to create.
discussion_topics (list): Discussion topic names. Picks ids and
sort_keys automatically.
cohorted_discussions: Discussion topics to cohort. Converts the
list to use the same ids as discussion topic names.
always_cohort_inline_discussions (bool): Whether inline discussions
should be cohorted by default.
Returns:
Nothing -- modifies course in place.
"""
def to_id(name):
"""Convert name to id."""
return topic_name_to_id(course, name)
set_course_cohort_settings(
set_course_cohorted(course.id, is_cohorted)
set_course_discussion_settings(
course.id,
is_cohorted=is_cohorted,
cohorted_discussions=[to_id(name) for name in cohorted_discussions],
always_cohort_inline_discussions=always_cohort_inline_discussions
division_scheme=discussion_division_scheme,
)
for cohort_name in auto_cohorts:
@@ -180,8 +137,6 @@ def config_course_cohorts(
cohort = CohortFactory(course_id=course.id, name=cohort_name)
CourseCohortFactory(course_user_group=cohort, assignment_type=CourseCohort.MANUAL)
course.discussion_topics = dict((name, {"sort_key": "A", "id": to_id(name)})
for name in discussion_topics)
try:
# Not implemented for XMLModulestore, which is used by test_cohorts.
modulestore().update_item(course, ModuleStoreEnum.UserID.test)

View File

@@ -5,13 +5,12 @@ Tests for cohorts
import ddt
from mock import call, patch
from nose.plugins.attrib import attr
import before_after
import before_after
from django.contrib.auth.models import User
from django.db import IntegrityError
from django.http import Http404
from django.test import TestCase
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
@@ -19,12 +18,9 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_MODULESTORE, ModuleStoreTestCase
from xmodule.modulestore.tests.factories import ToyCourseFactory
from ..models import CourseUserGroup, CourseCohort, CourseUserGroupPartitionGroup
from .. import cohorts
from ..tests.helpers import (
topic_name_to_id, config_course_cohorts, config_course_cohorts_legacy,
CohortFactory, CourseCohortFactory, CourseCohortSettingsFactory
)
from ..models import CourseCohort, CourseUserGroup, CourseUserGroupPartitionGroup
from ..tests.helpers import CohortFactory, CourseCohortFactory, config_course_cohorts, config_course_cohorts_legacy
@attr(shard=2)
@@ -350,7 +346,6 @@ class TestCohorts(ModuleStoreTestCase):
# This will have no effect on lms side as we are already done with migrations
config_course_cohorts_legacy(
course,
discussions=[],
cohorted=True,
auto_cohort_groups=["OtherGroup"]
)
@@ -393,7 +388,6 @@ class TestCohorts(ModuleStoreTestCase):
# This will have no effect on lms side as we are already done with migrations
config_course_cohorts_legacy(
course,
discussions=[],
cohorted=True,
auto_cohort_groups=["AutoGroup"]
)
@@ -475,44 +469,6 @@ class TestCohorts(ModuleStoreTestCase):
{cohort1.id: cohort1.name, cohort2.id: cohort2.name}
)
def test_get_cohorted_commentables(self):
"""
Make sure cohorts.get_cohorted_commentables() correctly returns a list of strings representing cohorted
commentables. Also verify that we can't get the cohorted commentables from a course which does not exist.
"""
course = modulestore().get_course(self.toy_course_key)
self.assertEqual(cohorts.get_cohorted_commentables(course.id), set())
config_course_cohorts(course, is_cohorted=True)
self.assertEqual(cohorts.get_cohorted_commentables(course.id), set())
config_course_cohorts(
course,
is_cohorted=True,
discussion_topics=["General", "Feedback"],
cohorted_discussions=["Feedback"]
)
self.assertItemsEqual(
cohorts.get_cohorted_commentables(course.id),
set([topic_name_to_id(course, "Feedback")])
)
config_course_cohorts(
course,
is_cohorted=True,
discussion_topics=["General", "Feedback"],
cohorted_discussions=["General", "Feedback"]
)
self.assertItemsEqual(
cohorts.get_cohorted_commentables(course.id),
set([topic_name_to_id(course, "General"), topic_name_to_id(course, "Feedback")])
)
self.assertRaises(
Http404,
lambda: cohorts.get_cohorted_commentables(SlashSeparatedCourseKey("course", "does_not", "exist"))
)
def test_get_cohort_by_name(self):
"""
Make sure cohorts.get_cohort_by_name() properly finds a cohort by name for a given course. Also verify that it
@@ -672,59 +628,16 @@ class TestCohorts(ModuleStoreTestCase):
# Note that the following get() will fail with MultipleObjectsReturned if race condition is not handled.
self.assertEqual(first_cohort.users.get(), course_user)
def test_get_course_cohort_settings(self):
def test_set_cohorted_with_invalid_data_type(self):
"""
Test that cohorts.get_course_cohort_settings is working as expected.
Test that cohorts.set_course_cohorted raises exception if argument is not a boolean.
"""
course = modulestore().get_course(self.toy_course_key)
course_cohort_settings = cohorts.get_course_cohort_settings(course.id)
self.assertFalse(course_cohort_settings.is_cohorted)
self.assertEqual(course_cohort_settings.cohorted_discussions, [])
self.assertFalse(course_cohort_settings.always_cohort_inline_discussions)
with self.assertRaises(ValueError) as value_error:
cohorts.set_course_cohorted(course.id, 'not a boolean')
def test_update_course_cohort_settings(self):
"""
Test that cohorts.set_course_cohort_settings is working as expected.
"""
course = modulestore().get_course(self.toy_course_key)
CourseCohortSettingsFactory(course_id=course.id)
cohorts.set_course_cohort_settings(
course.id,
is_cohorted=False,
cohorted_discussions=['topic a id', 'topic b id'],
always_cohort_inline_discussions=True
)
course_cohort_settings = cohorts.get_course_cohort_settings(course.id)
self.assertFalse(course_cohort_settings.is_cohorted)
self.assertEqual(course_cohort_settings.cohorted_discussions, ['topic a id', 'topic b id'])
self.assertTrue(course_cohort_settings.always_cohort_inline_discussions)
def test_update_course_cohort_settings_with_invalid_data_type(self):
"""
Test that cohorts.set_course_cohort_settings raises exception if fields have incorrect data type.
"""
course = modulestore().get_course(self.toy_course_key)
CourseCohortSettingsFactory(course_id=course.id)
exception_msg_tpl = "Incorrect field type for `{}`. Type must be `{}`"
fields = [
{'name': 'is_cohorted', 'type': bool},
{'name': 'always_cohort_inline_discussions', 'type': bool},
{'name': 'cohorted_discussions', 'type': list}
]
for field in fields:
with self.assertRaises(ValueError) as value_error:
cohorts.set_course_cohort_settings(course.id, **{field['name']: ''})
self.assertEqual(
value_error.exception.message,
exception_msg_tpl.format(field['name'], field['type'].__name__)
)
self.assertEqual("Cohorted must be a boolean", value_error.exception.message)
@attr(shard=2)

View File

@@ -4,37 +4,32 @@ Tests for course group views
# pylint: disable=attribute-defined-outside-init
# pylint: disable=no-member
import json
from collections import namedtuple
from datetime import datetime
from nose.plugins.attrib import attr
from django.contrib.auth.models import User
from django.http import Http404
from django.test.client import RequestFactory
from django_comment_common.models import CourseDiscussionSettings
from django_comment_common.utils import get_course_discussion_settings
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.modulestore.tests.factories import ItemFactory
from lms.djangoapps.django_comment_client.constants import TYPE_ENTRY, TYPE_SUBCATEGORY
from openedx.core.djangolib.testing.utils import skip_unless_lms
from ..models import CourseUserGroup, CourseCohort
from ..cohorts import DEFAULT_COHORT_NAME, get_cohort, get_cohort_by_id, get_cohort_by_name, get_group_info_for_cohort
from ..models import CourseCohort, CourseUserGroup
from ..views import (
course_cohort_settings_handler, cohort_handler, users_in_cohort, add_users_to_cohort, remove_user_from_cohort,
link_cohort_to_partition_group, cohort_discussion_topics
)
from ..cohorts import (
get_cohort, get_cohort_by_name, get_cohort_by_id,
DEFAULT_COHORT_NAME, get_group_info_for_cohort
)
from .helpers import (
config_course_cohorts, config_course_cohorts_legacy, CohortFactory, CourseCohortFactory, topic_name_to_id
add_users_to_cohort,
cohort_handler,
course_cohort_settings_handler,
link_cohort_to_partition_group,
remove_user_from_cohort,
users_in_cohort
)
from .helpers import CohortFactory, CourseCohortFactory, config_course_cohorts, config_course_cohorts_legacy
@attr(shard=2)
@@ -99,36 +94,6 @@ class CohortViewsTestCase(ModuleStoreTestCase):
view_args.insert(0, request)
self.assertRaises(Http404, view, *view_args)
def create_cohorted_discussions(self):
"""
Set up a cohorted discussion in the system, complete with all the fixings
"""
cohorted_inline_discussions = ['Topic A']
cohorted_course_wide_discussions = ["Topic B"]
cohorted_discussions = cohorted_inline_discussions + cohorted_course_wide_discussions
# inline discussion
ItemFactory.create(
parent_location=self.course.location,
category="discussion",
discussion_id=topic_name_to_id(self.course, "Topic A"),
discussion_category="Chapter",
discussion_target="Discussion",
start=datetime.now()
)
# course-wide discussion
discussion_topics = {
"Topic B": {"id": "Topic B"},
}
config_course_cohorts(
self.course,
is_cohorted=True,
discussion_topics=discussion_topics,
cohorted_discussions=cohorted_discussions
)
return cohorted_inline_discussions, cohorted_course_wide_discussions
def get_handler(self, course, cohort=None, expected_response_code=200, handler=cohort_handler):
"""
Call a GET on `handler` for a given `course` and return its response as a dict.
@@ -189,9 +154,6 @@ class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase):
"""
return {
'is_cohorted': True,
'always_cohort_inline_discussions': False,
'cohorted_inline_discussions': [],
'cohorted_course_wide_discussions': [],
'id': 1
}
@@ -202,22 +164,6 @@ class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase):
self._verify_non_staff_cannot_access(course_cohort_settings_handler, "GET", [unicode(self.course.id)])
self._verify_non_staff_cannot_access(course_cohort_settings_handler, "PATCH", [unicode(self.course.id)])
def test_get_settings(self):
"""
Verify that course_cohort_settings_handler is working for HTTP GET.
"""
cohorted_inline_discussions, cohorted_course_wide_discussions = self.create_cohorted_discussions()
response = self.get_handler(self.course, handler=course_cohort_settings_handler)
expected_response = self.get_expected_response()
expected_response['cohorted_inline_discussions'] = [topic_name_to_id(self.course, name)
for name in cohorted_inline_discussions]
expected_response['cohorted_course_wide_discussions'] = [topic_name_to_id(self.course, name)
for name in cohorted_course_wide_discussions]
self.assertEqual(response, expected_response)
def test_update_is_cohorted_settings(self):
"""
Verify that course_cohort_settings_handler is working for is_cohorted via HTTP PATCH.
@@ -235,70 +181,29 @@ class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase):
self.assertEqual(response, expected_response)
def test_update_always_cohort_inline_discussion_settings(self):
def test_enabling_cohorts_does_not_change_division_scheme(self):
"""
Verify that course_cohort_settings_handler is working for always_cohort_inline_discussions via HTTP PATCH.
Verify that enabling cohorts on a course does not automatically set the discussion division_scheme
to cohort.
"""
config_course_cohorts(self.course, is_cohorted=True)
config_course_cohorts(self.course, is_cohorted=False, discussion_division_scheme=CourseDiscussionSettings.NONE)
response = self.get_handler(self.course, handler=course_cohort_settings_handler)
expected_response = self.get_expected_response()
expected_response['is_cohorted'] = False
self.assertEqual(response, expected_response)
expected_response['always_cohort_inline_discussions'] = True
response = self.patch_handler(self.course, data=expected_response, handler=course_cohort_settings_handler)
self.assertEqual(response, expected_response)
def test_update_course_wide_discussion_settings(self):
"""
Verify that course_cohort_settings_handler is working for cohorted_course_wide_discussions via HTTP PATCH.
"""
# course-wide discussion
discussion_topics = {
"Topic B": {"id": "Topic B"},
}
config_course_cohorts(self.course, is_cohorted=True, discussion_topics=discussion_topics)
response = self.get_handler(self.course, handler=course_cohort_settings_handler)
expected_response = self.get_expected_response()
self.assertEqual(response, expected_response)
expected_response['cohorted_course_wide_discussions'] = [topic_name_to_id(self.course, "Topic B")]
response = self.patch_handler(self.course, data=expected_response, handler=course_cohort_settings_handler)
self.assertEqual(response, expected_response)
def test_update_inline_discussion_settings(self):
"""
Verify that course_cohort_settings_handler is working for cohorted_inline_discussions via HTTP PATCH.
"""
config_course_cohorts(self.course, is_cohorted=True)
response = self.get_handler(self.course, handler=course_cohort_settings_handler)
expected_response = self.get_expected_response()
self.assertEqual(response, expected_response)
now = datetime.now()
# inline discussion
ItemFactory.create(
parent_location=self.course.location,
category="discussion",
discussion_id="Topic_A",
discussion_category="Chapter",
discussion_target="Discussion",
start=now
self.assertEqual(
CourseDiscussionSettings.NONE, get_course_discussion_settings(self.course.id).division_scheme
)
expected_response['cohorted_inline_discussions'] = ["Topic_A"]
expected_response['is_cohorted'] = True
response = self.patch_handler(self.course, data=expected_response, handler=course_cohort_settings_handler)
self.assertEqual(response, expected_response)
self.assertEqual(
CourseDiscussionSettings.NONE, get_course_discussion_settings(self.course.id).division_scheme
)
def test_update_settings_with_missing_field(self):
"""
@@ -322,7 +227,7 @@ class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase):
handler=course_cohort_settings_handler
)
self.assertEqual(
"Incorrect field type for `{}`. Type must be `{}`".format('is_cohorted', bool.__name__),
"Cohorted must be a boolean",
response.get("error")
)
@@ -447,7 +352,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
# set auto_cohort_groups
# these cohort config will have not effect on lms side as we are already done with migrations
config_course_cohorts_legacy(self.course, [], cohorted=True, auto_cohort_groups=["AutoGroup"])
config_course_cohorts_legacy(self.course, cohorted=True, auto_cohort_groups=["AutoGroup"])
# We should expect the DoesNotExist exception because above cohort config have
# no effect on lms side so as a result there will be no AutoGroup cohort present
@@ -1225,60 +1130,3 @@ class RemoveUserFromCohortTestCase(CohortViewsTestCase):
cohort = CohortFactory(course_id=self.course.id, users=[user])
response_dict = self.request_remove_user_from_cohort(user.username, cohort)
self.verify_removed_user_from_cohort(user.username, response_dict, cohort)
@attr(shard=2)
@skip_unless_lms
class CourseCohortDiscussionTopicsTestCase(CohortViewsTestCase):
"""
Tests the `cohort_discussion_topics` view.
"""
def test_non_staff(self):
"""
Verify that we cannot access cohort_discussion_topics if we're a non-staff user.
"""
self._verify_non_staff_cannot_access(cohort_discussion_topics, "GET", [unicode(self.course.id)])
def test_get_discussion_topics(self):
"""
Verify that course_cohort_settings_handler is working for HTTP GET.
"""
# create inline & course-wide discussion to verify the different map.
self.create_cohorted_discussions()
response = self.get_handler(self.course, handler=cohort_discussion_topics)
start_date = response['inline_discussions']['subcategories']['Chapter']['start_date']
expected_response = {
"course_wide_discussions": {
'children': [['Topic B', TYPE_ENTRY]],
'entries': {
'Topic B': {
'sort_key': 'A',
'is_divided': True,
'id': topic_name_to_id(self.course, "Topic B"),
'start_date': response['course_wide_discussions']['entries']['Topic B']['start_date']
}
}
},
"inline_discussions": {
'subcategories': {
'Chapter': {
'subcategories': {},
'children': [['Discussion', TYPE_ENTRY]],
'entries': {
'Discussion': {
'sort_key': None,
'is_divided': True,
'id': topic_name_to_id(self.course, "Topic A"),
'start_date': start_date
}
},
'sort_key': 'Chapter',
'start_date': start_date
}
},
'children': [['Chapter', TYPE_SUBCATEGORY]]
}
}
self.assertEqual(response, expected_response)

View File

@@ -5,6 +5,7 @@ Views related to course groups functionality.
import logging
import re
from courseware.courses import get_course_with_access
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core.paginator import EmptyPage, Paginator
@@ -14,13 +15,9 @@ from django.http import Http404, HttpResponseBadRequest
from django.utils.translation import ugettext
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods, require_POST
from edxmako.shortcuts import render_to_response
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from courseware.courses import get_course_with_access
from edxmako.shortcuts import render_to_response
from lms.djangoapps.django_comment_client.constants import TYPE_ENTRY
from lms.djangoapps.django_comment_client.utils import get_discussion_categories_ids, get_discussion_category_map
from util.json_request import JsonResponse, expect_json
from . import cohorts
@@ -63,20 +60,13 @@ def unlink_cohort_partition_group(cohort):
# pylint: disable=invalid-name
def _get_course_cohort_settings_representation(course, course_cohort_settings):
def _get_course_cohort_settings_representation(cohort_id, is_cohorted):
"""
Returns a JSON representation of a course cohort settings.
"""
cohorted_course_wide_discussions, cohorted_inline_discussions = get_cohorted_discussions(
course, course_cohort_settings
)
return {
'id': course_cohort_settings.id,
'is_cohorted': course_cohort_settings.is_cohorted,
'cohorted_inline_discussions': cohorted_inline_discussions,
'cohorted_course_wide_discussions': cohorted_course_wide_discussions,
'always_cohort_inline_discussions': course_cohort_settings.always_cohort_inline_discussions,
'id': cohort_id,
'is_cohorted': is_cohorted,
}
@@ -97,25 +87,6 @@ def _get_cohort_representation(cohort, course):
}
def get_cohorted_discussions(course, course_settings):
"""
Returns the course-wide and inline cohorted discussion ids separately.
"""
cohorted_course_wide_discussions = []
cohorted_inline_discussions = []
course_wide_discussions = [topic['id'] for __, topic in course.discussion_topics.items()]
all_discussions = get_discussion_categories_ids(course, None, include_all=True)
for cohorted_discussion_id in course_settings.cohorted_discussions:
if cohorted_discussion_id in course_wide_discussions:
cohorted_course_wide_discussions.append(cohorted_discussion_id)
elif cohorted_discussion_id in all_discussions:
cohorted_inline_discussions.append(cohorted_discussion_id)
return cohorted_course_wide_discussions, cohorted_inline_discussions
@require_http_methods(("GET", "PATCH"))
@ensure_csrf_cookie
@expect_json
@@ -130,45 +101,24 @@ def course_cohort_settings_handler(request, course_key_string):
Updates the cohort settings for the course. Returns the JSON representation of updated settings.
"""
course_key = CourseKey.from_string(course_key_string)
course = get_course_with_access(request.user, 'staff', course_key)
cohort_settings = cohorts.get_course_cohort_settings(course_key)
# Although this course data is not used this method will return 404 is user is not staff
get_course_with_access(request.user, 'staff', course_key)
if request.method == 'PATCH':
cohorted_course_wide_discussions, cohorted_inline_discussions = get_cohorted_discussions(
course, cohort_settings
)
settings_to_change = {}
if 'is_cohorted' in request.json:
settings_to_change['is_cohorted'] = request.json.get('is_cohorted')
if 'cohorted_course_wide_discussions' in request.json or 'cohorted_inline_discussions' in request.json:
cohorted_course_wide_discussions = request.json.get(
'cohorted_course_wide_discussions', cohorted_course_wide_discussions
)
cohorted_inline_discussions = request.json.get(
'cohorted_inline_discussions', cohorted_inline_discussions
)
settings_to_change['cohorted_discussions'] = cohorted_course_wide_discussions + cohorted_inline_discussions
if 'always_cohort_inline_discussions' in request.json:
settings_to_change['always_cohort_inline_discussions'] = request.json.get(
'always_cohort_inline_discussions'
)
if not settings_to_change:
if 'is_cohorted' not in request.json:
return JsonResponse({"error": unicode("Bad Request")}, 400)
is_cohorted = request.json.get('is_cohorted')
try:
cohort_settings = cohorts.set_course_cohort_settings(
course_key, **settings_to_change
)
cohorts.set_course_cohorted(course_key, is_cohorted)
except ValueError as err:
# Note: error message not translated because it is not exposed to the user (UI prevents this state).
return JsonResponse({"error": unicode(err)}, 400)
return JsonResponse(_get_course_cohort_settings_representation(course, cohort_settings))
return JsonResponse(_get_course_cohort_settings_representation(
cohorts.get_course_cohort_id(course_key),
cohorts.is_course_cohorted(course_key)
))
@require_http_methods(("GET", "PUT", "POST", "PATCH"))
@@ -417,81 +367,3 @@ def debug_cohort_mgmt(request, course_key_string):
kwargs={'course_key': course_key.to_deprecated_string()}
)}
return render_to_response('/course_groups/debug.html', context)
@expect_json
@login_required
def cohort_discussion_topics(request, course_key_string):
"""
The handler for cohort discussion categories requests.
This will raise 404 if user is not staff.
Returns the JSON representation of discussion topics w.r.t categories for the course.
Example:
>>> example = {
>>> "course_wide_discussions": {
>>> "entries": {
>>> "General": {
>>> "sort_key": "General",
>>> "is_divided": True,
>>> "id": "i4x-edx-eiorguegnru-course-foobarbaz"
>>> }
>>> }
>>> "children": ["General", "entry"]
>>> },
>>> "inline_discussions" : {
>>> "subcategories": {
>>> "Getting Started": {
>>> "subcategories": {},
>>> "children": [
>>> ["Working with Videos", "entry"],
>>> ["Videos on edX", "entry"]
>>> ],
>>> "entries": {
>>> "Working with Videos": {
>>> "sort_key": None,
>>> "is_divided": False,
>>> "id": "d9f970a42067413cbb633f81cfb12604"
>>> },
>>> "Videos on edX": {
>>> "sort_key": None,
>>> "is_divided": False,
>>> "id": "98d8feb5971041a085512ae22b398613"
>>> }
>>> }
>>> },
>>> "children": ["Getting Started", "subcategory"]
>>> },
>>> }
>>> }
"""
course_key = CourseKey.from_string(course_key_string)
course = get_course_with_access(request.user, 'staff', course_key)
discussion_topics = {}
discussion_category_map = get_discussion_category_map(
course, request.user, divided_only_if_explicit=True, exclude_unstarted=False
)
# We extract the data for the course wide discussions from the category map.
course_wide_entries = discussion_category_map.pop('entries')
course_wide_children = []
inline_children = []
for name, c_type in discussion_category_map['children']:
if name in course_wide_entries and c_type == TYPE_ENTRY:
course_wide_children.append([name, c_type])
else:
inline_children.append([name, c_type])
discussion_topics['course_wide_discussions'] = {
'entries': course_wide_entries,
'children': course_wide_children
}
discussion_category_map['children'] = inline_children
discussion_topics['inline_discussions'] = discussion_category_map
return JsonResponse(discussion_topics)

View File

@@ -1,19 +1,17 @@
"""
UserPartitionScheme for enrollment tracks.
"""
from django.conf import settings
from course_modes.models import CourseMode
from courseware.masquerade import (
get_course_masquerade,
get_masquerading_user_group,
is_masquerading_as_specific_student
)
from course_modes.models import CourseMode
from student.models import CourseEnrollment
from django.conf import settings
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.verified_track_content.models import VerifiedTrackCohortedCourse
from xmodule.partitions.partitions import NoSuchUserPartitionGroupError, Group, UserPartition
from student.models import CourseEnrollment
from xmodule.partitions.partitions import Group, UserPartition
# These IDs must be less than 100 so that they do not overlap with Groups in
# CohortUserPartition or RandomUserPartitionScheme

View File

@@ -4,23 +4,26 @@ Tests for Verified Track Cohorting models
# pylint: disable=attribute-defined-outside-init
# pylint: disable=no-member
from django.test import TestCase
import mock
from mock import patch
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.course_groups.cohorts import get_cohort
from student.models import CourseMode
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from ..models import VerifiedTrackCohortedCourse, DEFAULT_VERIFIED_COHORT_NAME
from ..tasks import sync_cohort_with_mode
from openedx.core.djangoapps.course_groups.cohorts import (
set_course_cohort_settings, add_cohort, CourseCohort, DEFAULT_COHORT_NAME
DEFAULT_COHORT_NAME,
CourseCohort,
add_cohort,
get_cohort,
set_course_cohorted
)
from openedx.core.djangolib.testing.utils import skip_unless_lms
from student.models import CourseMode
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from ..models import DEFAULT_VERIFIED_COHORT_NAME, VerifiedTrackCohortedCourse
from ..tasks import sync_cohort_with_mode
class TestVerifiedTrackCohortedCourse(TestCase):
@@ -88,7 +91,7 @@ class TestMoveToVerified(SharedModuleStoreTestCase):
def _enable_cohorting(self):
""" Turn on cohorting in the course. """
set_course_cohort_settings(self.course.id, is_cohorted=True)
set_course_cohorted(self.course.id, True)
def _create_verified_cohort(self, name=DEFAULT_VERIFIED_COHORT_NAME):
""" Create a verified cohort. """