Commit 28d743e2 by cahrens

Allow the "Default Group" to be renamed.

TNL-4355
parent c7bc0490
...@@ -12,7 +12,7 @@ from courseware.courses import get_course_by_id ...@@ -12,7 +12,7 @@ from courseware.courses import get_course_by_id
from verified_track_content.tasks import sync_cohort_with_mode from verified_track_content.tasks import sync_cohort_with_mode
from openedx.core.djangoapps.course_groups.cohorts import ( from openedx.core.djangoapps.course_groups.cohorts import (
get_course_cohorts, CourseCohort, is_course_cohorted get_course_cohorts, CourseCohort, is_course_cohorted, get_random_cohort, is_default_cohort
) )
import logging import logging
...@@ -34,26 +34,39 @@ def move_to_verified_cohort(sender, instance, **kwargs): # pylint: disable=unus ...@@ -34,26 +34,39 @@ def move_to_verified_cohort(sender, instance, **kwargs): # pylint: disable=unus
if verified_cohort_enabled and (instance.mode != instance._old_mode): # pylint: disable=protected-access if verified_cohort_enabled and (instance.mode != instance._old_mode): # pylint: disable=protected-access
if not is_course_cohorted(course_key): if not is_course_cohorted(course_key):
log.error("Automatic verified cohorting enabled for course '%s', but course is not cohorted", course_key) log.error("Automatic verified cohorting enabled for course '%s', but course is not cohorted.", course_key)
else: else:
existing_cohorts = get_course_cohorts(get_course_by_id(course_key), CourseCohort.MANUAL) course = get_course_by_id(course_key)
if any(cohort.name == verified_cohort_name for cohort in existing_cohorts): existing_manual_cohorts = get_course_cohorts(course, CourseCohort.MANUAL)
args = { if any(cohort.name == verified_cohort_name for cohort in existing_manual_cohorts):
'course_id': unicode(course_key), # Verify that a single random cohort exists in the course. Note that calling this method will create
'user_id': instance.user.id, # a "Default Group" random cohort if no random cohorts exist yet.
'verified_cohort_name': verified_cohort_name random_cohort = get_random_cohort(course_key)
} if not is_default_cohort(random_cohort):
# Do the update with a 3-second delay in hopes that the CourseEnrollment transaction has been log.error(
# completed before the celery task runs. We want a reasonably short delay in case the learner "Automatic verified cohorting enabled for course '%s', "
# immediately goes to the courseware. "but course does not have exactly one default cohort for audit learners.",
sync_cohort_with_mode.apply_async(kwargs=args, countdown=3) course_key
)
# In case the transaction actually was not committed before the celery task runs, else:
# run it again after 5 minutes. If the first completed successfully, this task will be a no-op. args = {
sync_cohort_with_mode.apply_async(kwargs=args, countdown=300) 'course_id': unicode(course_key),
'user_id': instance.user.id,
'verified_cohort_name': verified_cohort_name,
'default_cohort_name': random_cohort.name
}
# Do the update with a 3-second delay in hopes that the CourseEnrollment transaction has been
# completed before the celery task runs. We want a reasonably short delay in case the learner
# immediately goes to the courseware.
sync_cohort_with_mode.apply_async(kwargs=args, countdown=3)
# In case the transaction actually was not committed before the celery task runs,
# run it again after 5 minutes. If the first completed successfully, this task will be a no-op.
sync_cohort_with_mode.apply_async(kwargs=args, countdown=300)
else: else:
log.error( log.error(
"Automatic verified cohorting enabled for course '%s', but cohort named '%s' does not exist.", "Automatic verified cohorting enabled for course '%s', "
"but verified cohort named '%s' does not exist.",
course_key, course_key,
verified_cohort_name, verified_cohort_name,
) )
......
...@@ -9,14 +9,14 @@ from celery.utils.log import get_task_logger ...@@ -9,14 +9,14 @@ from celery.utils.log import get_task_logger
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from student.models import CourseEnrollment, CourseMode from student.models import CourseEnrollment, CourseMode
from openedx.core.djangoapps.course_groups.cohorts import ( from openedx.core.djangoapps.course_groups.cohorts import (
get_cohort_by_name, get_cohort, add_user_to_cohort, DEFAULT_COHORT_NAME get_cohort_by_name, get_cohort, add_user_to_cohort
) )
LOGGER = get_task_logger(__name__) LOGGER = get_task_logger(__name__)
@task() @task()
def sync_cohort_with_mode(course_id, user_id, verified_cohort_name): def sync_cohort_with_mode(course_id, user_id, verified_cohort_name, default_cohort_name):
""" """
If the learner's mode does not match their assigned cohort, move the learner into the correct cohort. If the learner's mode does not match their assigned cohort, move the learner into the correct cohort.
It is assumed that this task is only initiated for courses that are using the It is assumed that this task is only initiated for courses that are using the
...@@ -34,17 +34,20 @@ def sync_cohort_with_mode(course_id, user_id, verified_cohort_name): ...@@ -34,17 +34,20 @@ def sync_cohort_with_mode(course_id, user_id, verified_cohort_name):
if enrollment.mode == CourseMode.VERIFIED and (current_cohort.id != verified_cohort.id): if enrollment.mode == CourseMode.VERIFIED and (current_cohort.id != verified_cohort.id):
LOGGER.info( LOGGER.info(
"MOVING_TO_VERIFIED: Moving user '%s' to the verified cohort for course '%s'", user.username, course_id "MOVING_TO_VERIFIED: Moving user '%s' to the verified cohort '%s' for course '%s'",
user.username, verified_cohort.name, course_id
) )
add_user_to_cohort(verified_cohort, user.username) add_user_to_cohort(verified_cohort, user.username)
elif enrollment.mode != CourseMode.VERIFIED and current_cohort.id == verified_cohort.id: elif enrollment.mode != CourseMode.VERIFIED and current_cohort.id == verified_cohort.id:
default_cohort = get_cohort_by_name(course_key, default_cohort_name)
LOGGER.info( LOGGER.info(
"MOVING_TO_DEFAULT: Moving user '%s' to the default cohort for course '%s'", user.username, course_id "MOVING_TO_DEFAULT: Moving user '%s' to the default cohort '%s' for course '%s'",
user.username, default_cohort.name, course_id
) )
default_cohort = get_cohort_by_name(course_key, DEFAULT_COHORT_NAME)
add_user_to_cohort(default_cohort, user.username) add_user_to_cohort(default_cohort, user.username)
else: else:
LOGGER.info( LOGGER.info(
"NO_ACTION_NECESSARY: No action necessary for user '%s' in course '%s' and enrollment mode '%s'", "NO_ACTION_NECESSARY: No action necessary for user '%s' in course '%s' and enrollment mode '%s'. "
user.username, course_id, enrollment.mode "The user is already in cohort '%s'.",
user.username, course_id, enrollment.mode, current_cohort.name
) )
...@@ -86,6 +86,9 @@ class TestMoveToVerified(SharedModuleStoreTestCase): ...@@ -86,6 +86,9 @@ class TestMoveToVerified(SharedModuleStoreTestCase):
def _create_verified_cohort(self, name=DEFAULT_VERIFIED_COHORT_NAME): def _create_verified_cohort(self, name=DEFAULT_VERIFIED_COHORT_NAME):
add_cohort(self.course.id, name, CourseCohort.MANUAL) add_cohort(self.course.id, name, CourseCohort.MANUAL)
def _create_named_random_cohort(self, name):
return add_cohort(self.course.id, name, CourseCohort.RANDOM)
def _enable_verified_track_cohorting(self, cohort_name=None): def _enable_verified_track_cohorting(self, cohort_name=None):
""" Enable verified track cohorting for the default course. """ """ Enable verified track cohorting for the default course. """
if cohort_name: if cohort_name:
...@@ -162,6 +165,27 @@ class TestMoveToVerified(SharedModuleStoreTestCase): ...@@ -162,6 +165,27 @@ class TestMoveToVerified(SharedModuleStoreTestCase):
error_message = "cohort named '%s' does not exist" error_message = "cohort named '%s' does not exist"
self.assertIn(error_message, error_logger.call_args[0][0]) self.assertIn(error_message, error_logger.call_args[0][0])
@mock.patch('verified_track_content.models.log.error')
def test_cohorting_enabled_too_many_random_cohorts(self, error_logger):
"""
If the VerifiedTrackCohortedCourse feature is enabled for a course and the course is cohorted,
but the course has > 1 random cohorts, an error is logged and enrollment mode changes do not
move learners into a cohort.
"""
# Enable cohorting, and create the verified cohort.
self._enable_cohorting()
self._create_verified_cohort()
# Create two random cohorts.
self._create_named_random_cohort("Random 1")
self._create_named_random_cohort("Random 2")
# Enable verified track cohorting feature
self._enable_verified_track_cohorting()
self.assertTrue(VerifiedTrackCohortedCourse.is_verified_track_cohort_enabled(self.course.id))
self._verify_no_automatic_cohorting()
self.assertTrue(error_logger.called)
error_message = "course does not have exactly one default cohort"
self.assertIn(error_message, error_logger.call_args[0][0])
def test_automatic_cohorting_enabled(self): def test_automatic_cohorting_enabled(self):
""" """
If the VerifiedTrackCohortedCourse feature is enabled for a course (with course cohorting enabled If the VerifiedTrackCohortedCourse feature is enabled for a course (with course cohorting enabled
...@@ -205,10 +229,43 @@ class TestMoveToVerified(SharedModuleStoreTestCase): ...@@ -205,10 +229,43 @@ class TestMoveToVerified(SharedModuleStoreTestCase):
self.assertEqual(DEFAULT_VERIFIED_COHORT_NAME, get_cohort(self.user, self.course.id, assign=False).name) self.assertEqual(DEFAULT_VERIFIED_COHORT_NAME, get_cohort(self.user, self.course.id, assign=False).name)
def test_custom_verified_cohort_name(self): def test_custom_verified_cohort_name(self):
CUSTOM_COHORT_NAME = 'special verified cohort' """
Test that enrolling in verified works correctly when the "verified cohort" has a custom name.
"""
custom_cohort_name = 'special verified cohort'
self._enable_cohorting() self._enable_cohorting()
self._create_verified_cohort(name=CUSTOM_COHORT_NAME) self._create_verified_cohort(name=custom_cohort_name)
self._enable_verified_track_cohorting(cohort_name=CUSTOM_COHORT_NAME) self._enable_verified_track_cohorting(cohort_name=custom_cohort_name)
self._enroll_in_course() self._enroll_in_course()
self._upgrade_to_verified() self._upgrade_to_verified()
self.assertEqual(CUSTOM_COHORT_NAME, get_cohort(self.user, self.course.id, assign=False).name) self.assertEqual(custom_cohort_name, get_cohort(self.user, self.course.id, assign=False).name)
def test_custom_default_cohort_name(self):
"""
Test that enrolling and un-enrolling works correctly when the single cohort
of type random has a different name from "Default Group".
"""
random_cohort_name = "custom random cohort"
self._enable_cohorting()
self._create_verified_cohort()
default_cohort = self._create_named_random_cohort(random_cohort_name)
self._enable_verified_track_cohorting()
self._enroll_in_course()
self.assertEqual(random_cohort_name, get_cohort(self.user, self.course.id, assign=False).name)
self._upgrade_to_verified()
self.assertEqual(DEFAULT_VERIFIED_COHORT_NAME, get_cohort(self.user, self.course.id, assign=False).name)
# Un-enroll from the course. The learner stays in the verified cohort, but is no longer active.
self._unenroll()
self.assertEqual(DEFAULT_VERIFIED_COHORT_NAME, get_cohort(self.user, self.course.id, assign=False).name)
# Change the name of the "default" cohort.
modified_cohort_name = "renamed random cohort"
default_cohort.name = modified_cohort_name
default_cohort.save()
# Re-enroll in the course, which will downgrade the learner to audit.
self._reenroll()
self.assertEqual(modified_cohort_name, get_cohort(self.user, self.course.id, assign=False).name)
self._upgrade_to_verified()
self.assertEqual(DEFAULT_VERIFIED_COHORT_NAME, get_cohort(self.user, self.course.id, assign=False).name)
...@@ -82,11 +82,11 @@ def _cohort_membership_changed(sender, **kwargs): ...@@ -82,11 +82,11 @@ def _cohort_membership_changed(sender, **kwargs):
# A 'default cohort' is an auto-cohort that is automatically created for a course if no cohort with automatic # A 'default cohort' is an auto-cohort that is automatically created for a course if no cohort with automatic
# assignment have been specified. It is intended to be used in a cohorted-course for users who have yet to be assigned # assignment have been specified. It is intended to be used in a cohorted course for users who have yet to be assigned
# to a cohort. # to a cohort, if the course staff have not explicitly created a cohort of type "RANDOM".
# Translation Note: We are NOT translating this string since it is the constant identifier for the "default group" # Note that course staff have the ability to change the name of this cohort after creation via the cohort
# and needed across product boundaries. # management UI in the instructor dashboard.
DEFAULT_COHORT_NAME = "Default Group" DEFAULT_COHORT_NAME = _("Default Group")
# tl;dr: global state is bad. capa reseeds random every time a problem is loaded. Even # tl;dr: global state is bad. capa reseeds random every time a problem is loaded. Even
...@@ -196,14 +196,17 @@ def get_cohort(user, course_key, assign=True, use_cached=False): ...@@ -196,14 +196,17 @@ def get_cohort(user, course_key, assign=True, use_cached=False):
# Otherwise assign the user a cohort. # Otherwise assign the user a cohort.
membership = CohortMembership.objects.create( membership = CohortMembership.objects.create(
user=user, user=user,
course_user_group=_get_default_cohort(course_key) course_user_group=get_random_cohort(course_key)
) )
return request_cache.data.setdefault(cache_key, membership.course_user_group) return request_cache.data.setdefault(cache_key, membership.course_user_group)
def _get_default_cohort(course_key): def get_random_cohort(course_key):
""" """
Helper method to get a default cohort for assignment in get_cohort Helper method to get a cohort for random assignment.
If there are multiple cohorts of type RANDOM in the course, one of them will be randomly selected.
If there are no existing cohorts of type RANDOM in the course, one will be created.
""" """
course = courses.get_course(course_key) course = courses.get_course(course_key)
cohorts = get_course_cohorts(course, assignment_type=CourseCohort.RANDOM) cohorts = get_course_cohorts(course, assignment_type=CourseCohort.RANDOM)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment