Merge pull request #15037 from edx/christina/ed11-simplification
Feature branch: dividing discussions by enrollment tracks
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. """
|
||||
|
||||
Reference in New Issue
Block a user