refactor: remove VerifiedTrackCohortedCourse feature
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
"""
|
||||
Django admin page for verified track configuration
|
||||
"""
|
||||
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from openedx.core.djangoapps.verified_track_content.forms import VerifiedTrackCourseForm
|
||||
from openedx.core.djangoapps.verified_track_content.models import (
|
||||
MigrateVerifiedTrackCohortsSetting,
|
||||
VerifiedTrackCohortedCourse
|
||||
)
|
||||
|
||||
|
||||
@admin.register(VerifiedTrackCohortedCourse)
|
||||
class VerifiedTrackCohortedCourseAdmin(admin.ModelAdmin):
|
||||
"""Admin for enabling verified track cohorting. """
|
||||
form = VerifiedTrackCourseForm
|
||||
|
||||
|
||||
@admin.register(MigrateVerifiedTrackCohortsSetting)
|
||||
class MigrateVerifiedTrackCohortsSettingAdmin(admin.ModelAdmin):
|
||||
"""Admin for configuring migration settings of verified track cohorting"""
|
||||
@@ -1,54 +0,0 @@
|
||||
"""
|
||||
Forms for configuring courses for verified track cohorting
|
||||
"""
|
||||
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from openedx.core.djangoapps.verified_track_content.models import VerifiedTrackCohortedCourse
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
|
||||
class VerifiedTrackCourseForm(forms.ModelForm):
|
||||
"""Validate course keys for the VerifiedTrackCohortedCourse model
|
||||
|
||||
The default behavior in Django admin is to:
|
||||
* Save course keys for courses that do not exist.
|
||||
* Return a 500 response if the course key format is invalid.
|
||||
|
||||
Using this form ensures that we display a user-friendly
|
||||
error message instead.
|
||||
|
||||
"""
|
||||
class Meta:
|
||||
model = VerifiedTrackCohortedCourse
|
||||
fields = '__all__'
|
||||
|
||||
def clean_course_key(self):
|
||||
"""Validate the course key.
|
||||
|
||||
Checks that the key format is valid and that
|
||||
the course exists. If not, displays an error message.
|
||||
|
||||
Arguments:
|
||||
field_name (str): The name of the field to validate.
|
||||
|
||||
Returns:
|
||||
CourseKey
|
||||
|
||||
"""
|
||||
cleaned_id = self.cleaned_data['course_key']
|
||||
error_msg = _('COURSE NOT FOUND. Please check that the course ID is valid.')
|
||||
|
||||
try:
|
||||
course_key = CourseKey.from_string(cleaned_id)
|
||||
except InvalidKeyError:
|
||||
raise forms.ValidationError(error_msg) # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
if not modulestore().has_course(course_key):
|
||||
raise forms.ValidationError(error_msg)
|
||||
|
||||
return course_key
|
||||
@@ -1,283 +0,0 @@
|
||||
"""Management command to migrate a course's xblock's group_access from Verified Track Cohorts to Enrollment Tracks"""
|
||||
|
||||
|
||||
from textwrap import dedent
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from cms.djangoapps.contentstore.course_group_config import GroupConfiguration
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from openedx.core.djangoapps.course_groups.cohorts import CourseCohort
|
||||
from openedx.core.djangoapps.course_groups.models import CourseUserGroup, CourseUserGroupPartitionGroup
|
||||
from openedx.core.djangoapps.verified_track_content.models import (
|
||||
MigrateVerifiedTrackCohortsSetting,
|
||||
VerifiedTrackCohortedCourse
|
||||
)
|
||||
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.partitions.partitions_service import PartitionService # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Migrates a course's xblock's group_access from Verified Track Cohorts to Enrollment Tracks
|
||||
"""
|
||||
help = dedent(__doc__).strip()
|
||||
|
||||
def handle(self, *args, **options): # lint-amnesty, pylint: disable=too-many-statements
|
||||
|
||||
errors = []
|
||||
|
||||
module_store = modulestore()
|
||||
|
||||
print("Starting Swap from Auto Track Cohort Pilot command")
|
||||
|
||||
verified_track_cohorts_setting = self._latest_settings()
|
||||
|
||||
if not verified_track_cohorts_setting:
|
||||
raise CommandError("No MigrateVerifiedTrackCohortsSetting found")
|
||||
|
||||
if not verified_track_cohorts_setting.enabled:
|
||||
raise CommandError("No enabled MigrateVerifiedTrackCohortsSetting found")
|
||||
|
||||
old_course_key = verified_track_cohorts_setting.old_course_key
|
||||
rerun_course_key = verified_track_cohorts_setting.rerun_course_key
|
||||
audit_cohort_names = verified_track_cohorts_setting.get_audit_cohort_names()
|
||||
|
||||
# Verify that the MigrateVerifiedTrackCohortsSetting has all required fields
|
||||
if not old_course_key:
|
||||
raise CommandError("No old_course_key set for MigrateVerifiedTrackCohortsSetting with ID: '%s'"
|
||||
% verified_track_cohorts_setting.id)
|
||||
|
||||
if not rerun_course_key:
|
||||
raise CommandError("No rerun_course_key set for MigrateVerifiedTrackCohortsSetting with ID: '%s'"
|
||||
% verified_track_cohorts_setting.id)
|
||||
|
||||
if not audit_cohort_names:
|
||||
raise CommandError("No audit_cohort_names set for MigrateVerifiedTrackCohortsSetting with ID: '%s'"
|
||||
% verified_track_cohorts_setting.id)
|
||||
|
||||
print("Running for MigrateVerifiedTrackCohortsSetting with old_course_key='%s' and rerun_course_key='%s'" %
|
||||
(verified_track_cohorts_setting.old_course_key, verified_track_cohorts_setting.rerun_course_key))
|
||||
|
||||
# Get the CourseUserGroup IDs for the audit course names from the old course
|
||||
audit_course_user_group_ids = CourseUserGroup.objects.filter(
|
||||
name__in=audit_cohort_names,
|
||||
course_id=old_course_key,
|
||||
group_type=CourseUserGroup.COHORT,
|
||||
).values_list('id', flat=True)
|
||||
|
||||
if not audit_course_user_group_ids:
|
||||
raise CommandError(
|
||||
"No Audit CourseUserGroup found for course_id='%s' with group_type='%s' for names='%s'"
|
||||
% (old_course_key, CourseUserGroup.COHORT, audit_cohort_names)
|
||||
)
|
||||
|
||||
# Get all of the audit CourseCohorts from the above IDs that are RANDOM
|
||||
random_audit_course_user_group_ids = CourseCohort.objects.filter(
|
||||
course_user_group_id__in=audit_course_user_group_ids,
|
||||
assignment_type=CourseCohort.RANDOM
|
||||
).values_list('course_user_group_id', flat=True)
|
||||
|
||||
if not random_audit_course_user_group_ids:
|
||||
raise CommandError(
|
||||
"No Audit CourseCohorts found for course_user_group_ids='%s' with assignment_type='%s"
|
||||
% (audit_course_user_group_ids, CourseCohort.RANDOM)
|
||||
)
|
||||
|
||||
# Get the CourseUserGroupPartitionGroup for the above IDs, these contain the partition IDs and group IDs
|
||||
# that are set for group_access inside of modulestore
|
||||
random_audit_course_user_group_partition_groups = list(CourseUserGroupPartitionGroup.objects.filter(
|
||||
course_user_group_id__in=random_audit_course_user_group_ids
|
||||
))
|
||||
|
||||
if not random_audit_course_user_group_partition_groups:
|
||||
raise CommandError(
|
||||
"No Audit CourseUserGroupPartitionGroup found for course_user_group_ids='%s'"
|
||||
% random_audit_course_user_group_ids
|
||||
)
|
||||
|
||||
# Get the single VerifiedTrackCohortedCourse for the old course
|
||||
try:
|
||||
verified_track_cohorted_course = VerifiedTrackCohortedCourse.objects.get(course_key=old_course_key)
|
||||
except VerifiedTrackCohortedCourse.DoesNotExist:
|
||||
raise CommandError("No VerifiedTrackCohortedCourse found for course: '%s'" % old_course_key) # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
if not verified_track_cohorted_course.enabled:
|
||||
raise CommandError("VerifiedTrackCohortedCourse not enabled for course: '%s'" % old_course_key)
|
||||
|
||||
# Get the single CourseUserGroupPartitionGroup for the verified_track
|
||||
# based on the verified_track name for the old course
|
||||
try:
|
||||
verified_course_user_group = CourseUserGroup.objects.get(
|
||||
course_id=old_course_key,
|
||||
group_type=CourseUserGroup.COHORT,
|
||||
name=verified_track_cohorted_course.verified_cohort_name
|
||||
)
|
||||
except CourseUserGroup.DoesNotExist:
|
||||
raise CommandError( # lint-amnesty, pylint: disable=raise-missing-from
|
||||
"No Verified CourseUserGroup found for course_id='%s' with group_type='%s' for names='%s'"
|
||||
% (old_course_key, CourseUserGroup.COHORT, verified_track_cohorted_course.verified_cohort_name)
|
||||
)
|
||||
|
||||
try:
|
||||
verified_course_user_group_partition_group = CourseUserGroupPartitionGroup.objects.get(
|
||||
course_user_group_id=verified_course_user_group.id
|
||||
)
|
||||
except CourseUserGroupPartitionGroup.DoesNotExist:
|
||||
raise CommandError( # lint-amnesty, pylint: disable=raise-missing-from
|
||||
"No Verified CourseUserGroupPartitionGroup found for course_user_group_ids='%s'"
|
||||
% random_audit_course_user_group_ids
|
||||
)
|
||||
|
||||
# Verify the enrollment track CourseModes exist for the new course
|
||||
try:
|
||||
CourseMode.objects.get(
|
||||
course_id=rerun_course_key,
|
||||
mode_slug=CourseMode.AUDIT
|
||||
)
|
||||
except CourseMode.DoesNotExist:
|
||||
raise CommandError("Audit CourseMode is not defined for course: '%s'" % rerun_course_key) # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
try:
|
||||
CourseMode.objects.get(
|
||||
course_id=rerun_course_key,
|
||||
mode_slug=CourseMode.VERIFIED
|
||||
)
|
||||
except CourseMode.DoesNotExist:
|
||||
raise CommandError("Verified CourseMode is not defined for course: '%s'" % rerun_course_key) # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
items = module_store.get_items(rerun_course_key)
|
||||
if not items:
|
||||
raise CommandError("Items for Course with key '%s' not found." % rerun_course_key)
|
||||
|
||||
items_to_update = []
|
||||
|
||||
all_cohorted_track_group_ids = set()
|
||||
for audit_course_user_group_partition_group in random_audit_course_user_group_partition_groups:
|
||||
all_cohorted_track_group_ids.add(audit_course_user_group_partition_group.group_id)
|
||||
all_cohorted_track_group_ids.add(verified_course_user_group_partition_group.group_id)
|
||||
|
||||
for item in items:
|
||||
# Verify that there exists group access for this xblock, otherwise skip these checks
|
||||
if item.group_access:
|
||||
set_audit_enrollment_track = False
|
||||
set_verified_enrollment_track = False
|
||||
|
||||
# Check the partition and group IDs for the audit course groups, if they exist in
|
||||
# the xblock's access settings then set the audit track flag to true
|
||||
for audit_course_user_group_partition_group in random_audit_course_user_group_partition_groups:
|
||||
audit_partition_group_access = item.group_access.get(
|
||||
audit_course_user_group_partition_group.partition_id,
|
||||
None
|
||||
)
|
||||
if (audit_partition_group_access
|
||||
and audit_course_user_group_partition_group.group_id in audit_partition_group_access):
|
||||
print("Queueing XBlock at location: '%s' for Audit Content Group update " % item.location)
|
||||
set_audit_enrollment_track = True
|
||||
|
||||
# Check the partition and group IDs for the verified course group, if it exists in
|
||||
# the xblock's access settings then set the verified track flag to true
|
||||
verified_partition_group_access = item.group_access.get(
|
||||
verified_course_user_group_partition_group.partition_id,
|
||||
None
|
||||
)
|
||||
if verified_partition_group_access:
|
||||
non_verified_track_access_groups = (set(verified_partition_group_access) -
|
||||
all_cohorted_track_group_ids)
|
||||
# If the item has group_access that is not the
|
||||
# verified or audit group IDs then raise an error
|
||||
# This only needs to be checked for this partition_group once
|
||||
if non_verified_track_access_groups:
|
||||
errors.append(
|
||||
"Non audit/verified cohorted content group set for xblock, location '%s' with IDs '%s'"
|
||||
% (item.location, non_verified_track_access_groups)
|
||||
)
|
||||
if verified_course_user_group_partition_group.group_id in verified_partition_group_access:
|
||||
print("Queueing XBlock at location: '%s' for Verified Content Group update " % item.location)
|
||||
set_verified_enrollment_track = True
|
||||
|
||||
# Add the enrollment track ids to a group access array
|
||||
enrollment_track_group_access = []
|
||||
if set_audit_enrollment_track:
|
||||
enrollment_track_group_access.append(settings.COURSE_ENROLLMENT_MODES['audit']['id'])
|
||||
if set_verified_enrollment_track:
|
||||
enrollment_track_group_access.append(settings.COURSE_ENROLLMENT_MODES['verified']['id'])
|
||||
|
||||
# If there are no errors, and either the audit track, or verified
|
||||
# track needed an update, set the access, update and publish
|
||||
if set_verified_enrollment_track or set_audit_enrollment_track:
|
||||
# Sets whether or not an xblock has changes
|
||||
has_changes = module_store.has_changes(item)
|
||||
|
||||
# Check that the xblock does not have changes and add it to be updated, otherwise add an error
|
||||
if not has_changes:
|
||||
item.group_access = {ENROLLMENT_TRACK_PARTITION_ID: enrollment_track_group_access}
|
||||
items_to_update.append(item)
|
||||
else:
|
||||
errors.append("XBlock '%s' with location '%s' needs access changes, but is a draft"
|
||||
% (item.display_name, item.location))
|
||||
|
||||
partitions_to_delete = random_audit_course_user_group_partition_groups
|
||||
partitions_to_delete.append(verified_course_user_group_partition_group)
|
||||
|
||||
# If there are no errors iterate over and update all of the items that had the access changed
|
||||
if not errors:
|
||||
for item in items_to_update:
|
||||
module_store.update_item(item, ModuleStoreEnum.UserID.mgmt_command)
|
||||
module_store.publish(item.location, ModuleStoreEnum.UserID.mgmt_command)
|
||||
print("Updated and published XBlock at location: '%s'" % item.location)
|
||||
|
||||
# Check if we should delete any partition groups if there are no errors.
|
||||
# If there are errors, none of the xblock items will have been updated,
|
||||
# so this section will throw errors for each partition in use
|
||||
if partitions_to_delete and not errors: # lint-amnesty, pylint: disable=too-many-nested-blocks
|
||||
partition_service = PartitionService(rerun_course_key)
|
||||
course = partition_service.get_course()
|
||||
for partition_to_delete in partitions_to_delete:
|
||||
# Get the user partition, and the index of that partition in the course
|
||||
partition = partition_service.get_user_partition(partition_to_delete.partition_id)
|
||||
if partition:
|
||||
partition_index = course.user_partitions.index(partition)
|
||||
group_id = int(partition_to_delete.group_id)
|
||||
|
||||
# Sanity check to verify that all of the groups being deleted are empty,
|
||||
# since they should have been converted to use enrollment tracks instead.
|
||||
# Taken from contentstore/views/course.py.remove_content_or_experiment_group
|
||||
usages = GroupConfiguration.get_partitions_usage_info(module_store, course)
|
||||
used = group_id in usages[partition.id]
|
||||
if used:
|
||||
errors.append("Content group '%s' is in use and cannot be deleted."
|
||||
% partition_to_delete.group_id)
|
||||
|
||||
# If there are not errors, proceed to update the course and user_partitions
|
||||
if not errors:
|
||||
# Remove the groups that match the group ID of the partition to be deleted
|
||||
# Else if there are no match groups left, remove the user partition
|
||||
matching_groups = [group for group in partition.groups if group.id == group_id]
|
||||
if matching_groups:
|
||||
group_index = partition.groups.index(matching_groups[0])
|
||||
partition.groups.pop(group_index)
|
||||
# Update the course user partition with the updated groups
|
||||
if partition.groups:
|
||||
course.user_partitions[partition_index] = partition
|
||||
else:
|
||||
course.user_partitions.pop(partition_index)
|
||||
module_store.update_item(course, ModuleStoreEnum.UserID.mgmt_command)
|
||||
|
||||
# If there are any errors, join them together and raise the CommandError
|
||||
if errors:
|
||||
raise CommandError(
|
||||
("Error for MigrateVerifiedTrackCohortsSetting with ID='%s'\n" % verified_track_cohorts_setting.id) +
|
||||
"\t\n".join(errors)
|
||||
)
|
||||
|
||||
print("Finished for MigrateVerifiedTrackCohortsSetting with ID='%s" % verified_track_cohorts_setting.id)
|
||||
|
||||
def _latest_settings(self):
|
||||
"""
|
||||
Return the latest version of the MigrateVerifiedTrackCohortsSetting
|
||||
"""
|
||||
return MigrateVerifiedTrackCohortsSetting.current()
|
||||
@@ -1,19 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
from opaque_keys.edx.django.models import CourseKeyField
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VerifiedTrackCohortedCourse',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('course_key', CourseKeyField(help_text='The course key for the course we would like to be auto-cohorted.', unique=True, max_length=255, db_index=True)),
|
||||
('enabled', models.BooleanField()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,16 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('verified_track_content', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='verifiedtrackcohortedcourse',
|
||||
name='verified_cohort_name',
|
||||
field=models.CharField(default='Verified Learners', max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from opaque_keys.edx.django.models import CourseKeyField
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('verified_track_content', '0002_verifiedtrackcohortedcourse_verified_cohort_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MigrateVerifiedTrackCohortsSetting',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
|
||||
('old_course_key', CourseKeyField(help_text='Course key for which to migrate verified track cohorts from', max_length=255)),
|
||||
('rerun_course_key', CourseKeyField(help_text='Course key for which to migrate verified track cohorts to enrollment tracks to', max_length=255)),
|
||||
('audit_cohort_names', models.TextField(help_text='Comma-separated list of audit cohort names')),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,185 +0,0 @@
|
||||
"""
|
||||
Models for verified track selections.
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
from config_models.models import ConfigurationModel
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from django.utils.translation import gettext_lazy
|
||||
from edx_django_utils.cache import RequestCache
|
||||
from opaque_keys.edx.django.models import CourseKeyField
|
||||
|
||||
from openedx.core.djangoapps.course_groups.cohorts import (
|
||||
CourseCohort,
|
||||
get_course_cohorts,
|
||||
get_random_cohort,
|
||||
is_course_cohorted
|
||||
)
|
||||
from openedx.core.djangoapps.verified_track_content.tasks import sync_cohort_with_mode
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
from openedx.core.lib.courses import get_course_by_id
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_VERIFIED_COHORT_NAME = "Verified Learners"
|
||||
|
||||
|
||||
@receiver(post_save, sender=CourseEnrollment)
|
||||
def move_to_verified_cohort(sender, instance, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
If the learner has changed modes, update assigned cohort iff the course is using
|
||||
the Automatic Verified Track Cohorting MVP feature.
|
||||
"""
|
||||
course_key = instance.course_id
|
||||
verified_cohort_enabled = VerifiedTrackCohortedCourse.is_verified_track_cohort_enabled(course_key)
|
||||
verified_cohort_name = VerifiedTrackCohortedCourse.verified_cohort_name_for_course(course_key)
|
||||
|
||||
if verified_cohort_enabled and (instance.mode != instance._old_mode): # pylint: disable=protected-access
|
||||
if not is_course_cohorted(course_key):
|
||||
log.error("Automatic verified cohorting enabled for course '%s', but course is not cohorted.", course_key)
|
||||
else:
|
||||
course = get_course_by_id(course_key)
|
||||
existing_manual_cohorts = get_course_cohorts(course, assignment_type=CourseCohort.MANUAL)
|
||||
if any(cohort.name == verified_cohort_name for cohort in existing_manual_cohorts):
|
||||
# Get a random cohort to use as the default cohort (for audit learners).
|
||||
# Note that calling this method will create a "Default Group" random cohort if no random
|
||||
# cohort yet exist.
|
||||
random_cohort = get_random_cohort(course_key)
|
||||
args = {
|
||||
'course_id': str(course_key),
|
||||
'user_id': instance.user.id,
|
||||
'verified_cohort_name': verified_cohort_name,
|
||||
'default_cohort_name': random_cohort.name
|
||||
}
|
||||
log.info(
|
||||
"Queuing automatic cohorting for user '%s' in course '%s' "
|
||||
"due to change in enrollment mode from '%s' to '%s'.",
|
||||
instance.user.id, course_key, instance._old_mode, instance.mode # pylint: disable=protected-access
|
||||
)
|
||||
|
||||
# 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:
|
||||
log.error(
|
||||
"Automatic verified cohorting enabled for course '%s', "
|
||||
"but verified cohort named '%s' does not exist.",
|
||||
course_key,
|
||||
verified_cohort_name,
|
||||
)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=CourseEnrollment)
|
||||
def pre_save_callback(sender, instance, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Extend to store previous mode.
|
||||
"""
|
||||
try:
|
||||
old_instance = sender.objects.get(pk=instance.pk)
|
||||
instance._old_mode = old_instance.mode # pylint: disable=protected-access
|
||||
except CourseEnrollment.DoesNotExist:
|
||||
instance._old_mode = None # pylint: disable=protected-access
|
||||
|
||||
|
||||
class VerifiedTrackCohortedCourse(models.Model):
|
||||
"""
|
||||
Tracks which courses have verified track auto-cohorting enabled.
|
||||
|
||||
.. no_pii:
|
||||
"""
|
||||
course_key = CourseKeyField(
|
||||
max_length=255, db_index=True, unique=True,
|
||||
help_text=gettext_lazy("The course key for the course we would like to be auto-cohorted.")
|
||||
)
|
||||
|
||||
verified_cohort_name = models.CharField(max_length=100, default=DEFAULT_VERIFIED_COHORT_NAME)
|
||||
|
||||
enabled = models.BooleanField()
|
||||
|
||||
CACHE_NAMESPACE = "verified_track_content.VerifiedTrackCohortedCourse.cache."
|
||||
|
||||
def __str__(self):
|
||||
return f"Course: {str(self.course_key)}, enabled: {self.enabled}"
|
||||
|
||||
@classmethod
|
||||
def verified_cohort_name_for_course(cls, course_key):
|
||||
"""
|
||||
Returns the given cohort name for the specific course.
|
||||
|
||||
Args:
|
||||
course_key (CourseKey): a course key representing the course we want the verified cohort name for
|
||||
|
||||
Returns:
|
||||
The cohort name if the course key has one associated to it. None otherwise.
|
||||
|
||||
"""
|
||||
try:
|
||||
config = cls.objects.get(course_key=course_key)
|
||||
return config.verified_cohort_name
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
@request_cached(namespace=CACHE_NAMESPACE)
|
||||
def is_verified_track_cohort_enabled(cls, course_key):
|
||||
"""
|
||||
Checks whether or not verified track cohort is enabled for the given course.
|
||||
|
||||
Args:
|
||||
course_key (CourseKey): a course key representing the course we want to check
|
||||
|
||||
Returns:
|
||||
True if the course has verified track cohorts is enabled
|
||||
False if not
|
||||
"""
|
||||
try:
|
||||
return cls.objects.get(course_key=course_key).enabled
|
||||
except cls.DoesNotExist:
|
||||
return False
|
||||
|
||||
|
||||
@receiver(models.signals.post_save, sender=VerifiedTrackCohortedCourse)
|
||||
@receiver(models.signals.post_delete, sender=VerifiedTrackCohortedCourse)
|
||||
def invalidate_verified_track_cache(sender, **kwargs): # pylint: disable=unused-argument
|
||||
"""Invalidate the cache of VerifiedTrackCohortedCourse. """
|
||||
RequestCache(namespace=VerifiedTrackCohortedCourse.CACHE_NAMESPACE).clear()
|
||||
|
||||
|
||||
class MigrateVerifiedTrackCohortsSetting(ConfigurationModel):
|
||||
"""
|
||||
Configuration for the swap_from_auto_track_cohorts management command.
|
||||
|
||||
.. no_pii:
|
||||
"""
|
||||
class Meta:
|
||||
app_label = "verified_track_content"
|
||||
|
||||
old_course_key = CourseKeyField(
|
||||
max_length=255,
|
||||
blank=False,
|
||||
help_text="Course key for which to migrate verified track cohorts from"
|
||||
)
|
||||
rerun_course_key = CourseKeyField(
|
||||
max_length=255,
|
||||
blank=False,
|
||||
help_text="Course key for which to migrate verified track cohorts to enrollment tracks to"
|
||||
)
|
||||
audit_cohort_names = models.TextField(
|
||||
help_text="Comma-separated list of audit cohort names"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_audit_cohort_names(cls):
|
||||
"""Get the list of audit cohort names for the course"""
|
||||
return [cohort_name for cohort_name in cls.current().audit_cohort_names.split(",") if cohort_name]
|
||||
@@ -14,7 +14,6 @@ from lms.djangoapps.courseware.masquerade import (
|
||||
get_masquerading_user_group,
|
||||
is_masquerading_as_specific_student
|
||||
)
|
||||
from openedx.core.djangoapps.verified_track_content.models import VerifiedTrackCohortedCourse
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from xmodule.partitions.partitions import Group, UserPartition # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
@@ -38,15 +37,9 @@ class EnrollmentTrackUserPartition(UserPartition):
|
||||
Return the groups (based on CourseModes) for the course associated with this
|
||||
EnrollmentTrackUserPartition instance. Note that only groups based on selectable
|
||||
CourseModes are returned (which means that Credit will never be returned).
|
||||
|
||||
If a course is using the Verified Track Cohorting pilot feature, this method
|
||||
returns an empty array regardless of registered CourseModes.
|
||||
"""
|
||||
course_key = CourseKey.from_string(self.parameters["course_id"])
|
||||
|
||||
if is_course_using_cohort_instead(course_key):
|
||||
return []
|
||||
|
||||
return [
|
||||
Group(ENROLLMENT_GROUP_IDS[mode.slug]["id"], str(mode.name))
|
||||
for mode in CourseMode.modes_for_course(course_key, include_expired=True)
|
||||
@@ -66,13 +59,7 @@ class EnrollmentTrackPartitionScheme:
|
||||
Returns the Group from the specified user partition to which the user
|
||||
is assigned, via enrollment mode. If a user is in a Credit mode, the Verified or
|
||||
Professional mode for the course is returned instead.
|
||||
|
||||
If a course is using the Verified Track Cohorting pilot feature, this method
|
||||
returns None regardless of the user's enrollment mode.
|
||||
"""
|
||||
if is_course_using_cohort_instead(course_key):
|
||||
return None
|
||||
|
||||
# First, check if we have to deal with masquerading.
|
||||
# If the current user is masquerading as a specific student, use the
|
||||
# same logic as normal to return that student's group. If the current
|
||||
@@ -124,11 +111,3 @@ class EnrollmentTrackPartitionScheme:
|
||||
parameters,
|
||||
active
|
||||
)
|
||||
|
||||
|
||||
def is_course_using_cohort_instead(course_key):
|
||||
"""
|
||||
Returns whether the given course_context is using verified-track cohorts
|
||||
and therefore shouldn't use a track-based partition.
|
||||
"""
|
||||
return VerifiedTrackCohortedCourse.is_verified_track_cohort_enabled(course_key)
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
"""
|
||||
Celery task for Automatic Verifed Track Cohorting MVP feature.
|
||||
"""
|
||||
|
||||
|
||||
from celery import shared_task
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from edx_django_utils.monitoring import set_code_owner_attribute
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, get_cohort, get_cohort_by_name
|
||||
from common.djangoapps.student.models import CourseEnrollment, CourseMode
|
||||
|
||||
LOGGER = get_task_logger(__name__)
|
||||
|
||||
|
||||
@shared_task(bind=True, default_retry_delay=60, max_retries=2)
|
||||
@set_code_owner_attribute
|
||||
def sync_cohort_with_mode(self, 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.
|
||||
It is assumed that this task is only initiated for courses that are using the
|
||||
Automatic Verified Track Cohorting MVP feature. It is also assumed that before
|
||||
initiating this task, verification has been done to ensure that the course is
|
||||
cohorted and has an appropriately named "verified" cohort.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
user = User.objects.get(id=user_id)
|
||||
try:
|
||||
enrollment = CourseEnrollment.get_enrollment(user, course_key)
|
||||
# Note that this will enroll the user in the default cohort on initial enrollment.
|
||||
# That's good because it will force creation of the default cohort if necessary.
|
||||
current_cohort = get_cohort(user, course_key)
|
||||
|
||||
verified_cohort = get_cohort_by_name(course_key, verified_cohort_name)
|
||||
|
||||
acceptable_modes = {CourseMode.VERIFIED, CourseMode.CREDIT_MODE}
|
||||
if enrollment.mode in acceptable_modes and (current_cohort.id != verified_cohort.id):
|
||||
LOGGER.info(
|
||||
"MOVING_TO_VERIFIED: Moving user '%s' to the verified cohort '%s' for course '%s'",
|
||||
user.id, verified_cohort.name, course_id
|
||||
)
|
||||
add_user_to_cohort(verified_cohort, user.username)
|
||||
elif enrollment.mode not in acceptable_modes and current_cohort.id == verified_cohort.id:
|
||||
default_cohort = get_cohort_by_name(course_key, default_cohort_name)
|
||||
LOGGER.info(
|
||||
"MOVING_TO_DEFAULT: Moving user '%s' to the default cohort '%s' for course '%s'",
|
||||
user.id, default_cohort.name, course_id
|
||||
)
|
||||
add_user_to_cohort(default_cohort, user.username)
|
||||
else:
|
||||
LOGGER.info(
|
||||
"NO_ACTION_NECESSARY: No action necessary for user '%s' in course '%s' and enrollment mode '%s'. "
|
||||
"The user is already in cohort '%s'.",
|
||||
user.id, course_id, enrollment.mode, current_cohort.name
|
||||
)
|
||||
except Exception as exc:
|
||||
LOGGER.warning(
|
||||
"SYNC_COHORT_WITH_MODE_RETRY: Exception encountered for course '%s' and user '%s': %s",
|
||||
course_id, user.id, str(exc)
|
||||
)
|
||||
raise self.retry(exc=exc)
|
||||
@@ -1,39 +0,0 @@
|
||||
"""
|
||||
View methods for verified track content.
|
||||
"""
|
||||
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from lms.djangoapps.courseware.courses import get_course_with_access
|
||||
from openedx.core.djangoapps.verified_track_content.models import VerifiedTrackCohortedCourse
|
||||
from common.djangoapps.util.json_request import JsonResponse, expect_json
|
||||
|
||||
|
||||
@expect_json
|
||||
@login_required
|
||||
def cohorting_settings(request, course_key_string):
|
||||
"""
|
||||
The handler for verified track cohorting requests.
|
||||
This will raise 404 if user is not staff.
|
||||
|
||||
Returns a JSON representation of whether or not the course has verified track cohorting enabled.
|
||||
The "verified_cohort_name" field will only be present if "enabled" is True.
|
||||
|
||||
Example:
|
||||
>>> example = {
|
||||
>>> "enabled": True,
|
||||
>>> "verified_cohort_name" : "Micromasters"
|
||||
>>> }
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
get_course_with_access(request.user, 'staff', course_key)
|
||||
|
||||
settings = {}
|
||||
verified_track_cohort_enabled = VerifiedTrackCohortedCourse.is_verified_track_cohort_enabled(course_key)
|
||||
settings['enabled'] = verified_track_cohort_enabled
|
||||
if verified_track_cohort_enabled:
|
||||
settings['verified_cohort_name'] = VerifiedTrackCohortedCourse.verified_cohort_name_for_course(course_key)
|
||||
|
||||
return JsonResponse(settings)
|
||||
Reference in New Issue
Block a user