refactor: remove VerifiedTrackCohortedCourse feature

This commit is contained in:
Eugene Dyudyunov
2022-05-02 17:41:08 +03:00
parent fcb29a6834
commit 5c5d383aa0
22 changed files with 16 additions and 967 deletions

View File

@@ -60,7 +60,6 @@ from openedx.core.djangoapps.discussions.utils import available_division_schemes
from openedx.core.djangoapps.django_comment_common.models import FORUM_ROLE_ADMINISTRATOR, CourseDiscussionSettings
from openedx.core.djangoapps.plugins.constants import ProjectType
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.verified_track_content.models import VerifiedTrackCohortedCourse
from openedx.core.djangolib.markup import HTML, Text
from openedx.core.lib.courses import get_course_by_id
from openedx.core.lib.url_utils import quote_slashes
@@ -508,9 +507,6 @@ def _section_cohort_management(course, access):
),
'cohorts_url': reverse('cohorts', kwargs={'course_key_string': str(course_key)}),
'upload_cohorts_csv_url': reverse('add_users_to_cohorts', kwargs={'course_id': str(course_key)}),
'verified_track_cohorting_url': reverse(
'verified_track_cohorting', kwargs={'course_key_string': str(course_key)}
),
}
return section_data
@@ -679,9 +675,7 @@ def _section_send_email(course, access):
cohorts = []
if is_course_cohorted(course_key):
cohorts = get_course_cohorts(course)
course_modes = []
if not VerifiedTrackCohortedCourse.is_verified_track_cohort_enabled(course_key):
course_modes = CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False)
course_modes = CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False)
email_editor = fragment.content
section_data = {
'section_key': 'send_email',

View File

@@ -1,12 +0,0 @@
(function(define) {
'use strict';
define(['backbone'], function(Backbone) {
var VerifiedTrackSettingsModel = Backbone.Model.extend({
defaults: {
enabled: false,
verified_cohort_name: ''
}
});
return VerifiedTrackSettingsModel;
});
}).call(this, define || RequireJS.define);

View File

@@ -1,19 +1,16 @@
(function(define) {
'use strict';
define(['jquery', 'underscore', 'backbone', 'gettext', 'js/groups/models/cohort',
'js/groups/models/verified_track_settings',
'js/groups/views/cohort_editor', 'js/groups/views/cohort_form',
'js/groups/views/course_cohort_settings_notification',
'js/groups/views/verified_track_settings_notification',
'edx-ui-toolkit/js/utils/html-utils',
'js/views/base_dashboard_view',
'js/views/file_uploader', 'js/models/notification', 'js/views/notification',
'string_utils'],
function($, _, Backbone, gettext, CohortModel,
VerifiedTrackSettingsModel,
CohortEditorView, CohortFormView,
CourseCohortSettingsNotificationView,
VerifiedTrackSettingsNotificationView, HtmlUtils, BaseDashboardView) {
CohortEditorView, CohortFormView,
CourseCohortSettingsNotificationView,
HtmlUtils, BaseDashboardView) {
var hiddenClass = 'hidden',
disabledClass = 'is-disabled',
enableCohortsSelector = '.cohorts-state';
@@ -52,19 +49,6 @@
cohortsEnabled: this.cohortSettings.get('is_cohorted')
}));
this.onSync();
// Don't create this view until the first render is called, as at that point the
// various other models whose state is required to properly view the notification
// will have completed their fetch operations.
if (!this.verifiedTrackSettingsNotificationView) {
var verifiedTrackSettingsModel = new VerifiedTrackSettingsModel();
verifiedTrackSettingsModel.url = this.context.verifiedTrackCohortingUrl;
verifiedTrackSettingsModel.fetch({
success: _.bind(this.renderVerifiedTrackSettingsNotificationView, this)
});
this.verifiedTrackSettingsNotificationView = new VerifiedTrackSettingsNotificationView({
model: verifiedTrackSettingsModel
});
}
return this;
},
@@ -83,14 +67,6 @@
cohortStateMessageNotificationView.render();
},
renderVerifiedTrackSettingsNotificationView: function() {
if (this.verifiedTrackSettingsNotificationView) {
this.verifiedTrackSettingsNotificationView.validateSettings(
this.getCohortsEnabled(), this.model.models, this.$(enableCohortsSelector)
);
}
},
onSync: function(model, response, options) {
var selectedCohort = this.lastSelectedCohortId && this.model.get(this.lastSelectedCohortId),
hasCohorts = this.model.length > 0,
@@ -124,7 +100,6 @@
actionIconClass: 'fa-plus'
});
}
this.renderVerifiedTrackSettingsNotificationView();
},
getSelectedCohort: function() {

View File

@@ -26,7 +26,6 @@
cohortSettings: courseCohortSettings,
context: {
uploadCohortsCsvUrl: $cohortManagementElement.data('upload_cohorts_csv_url'),
verifiedTrackCohortingUrl: $cohortManagementElement.data('verified_track_cohorting_url'),
studioGroupConfigurationsUrl: studioGroupConfigurationsUrl,
isCcxEnabled: $cohortManagementElement.data('is_ccx_enabled')
}

View File

@@ -1,76 +0,0 @@
(function(define) {
'use strict';
define(['jquery', 'underscore', 'backbone', 'gettext', 'edx-ui-toolkit/js/utils/string-utils',
'js/models/notification', 'js/views/notification'],
function($, _, Backbone, gettext, StringUtils) {
/* global NotificationModel, NotificationView */
var VerifiedTrackSettingsNotificationView = Backbone.View.extend({
render: function() {
// All rendering is done in validateSettings, which must be called with some additional information.
return this;
},
validateSettings: function(isCohorted, cohortCollection, enableCohortsCheckbox) {
if (this.model.get('enabled')) {
var verifiedCohortName = this.model.get('verified_cohort_name');
if (isCohorted) {
var verifiedCohortExists = false;
$.each(cohortCollection, function(_, cohort) {
if (cohort.get('assignment_type') === 'manual' &&
cohort.get('name') === verifiedCohortName) {
verifiedCohortExists = true;
cohort.disableEditingName = true;
} else {
cohort.disableEditingName = false;
}
}
);
if (verifiedCohortExists) {
this.showNotification({
type: 'confirmation',
title: StringUtils.interpolate(
gettext("This course uses automatic cohorting for verified track learners. You cannot disable cohorts, and you cannot rename the manual cohort named '{verifiedCohortName}'. To change the configuration for verified track cohorts, contact your edX partner manager."), // eslint-disable-line max-len
{verifiedCohortName: verifiedCohortName}
)
});
} else {
this.showNotification({
type: 'error',
title: StringUtils.interpolate(
gettext("This course has automatic cohorting enabled for verified track learners, but the required cohort does not exist. You must create a manually-assigned cohort named '{verifiedCohortName}' for the feature to work."), // eslint-disable-line max-len
{verifiedCohortName: verifiedCohortName}
)
});
}
enableCohortsCheckbox.prop('disabled', true);
} else {
this.showNotification({
type: 'error',
title: gettext('This course has automatic cohorting enabled for verified track learners, but cohorts are disabled. You must enable cohorts for the feature to work.') // eslint-disable-line max-len
});
enableCohortsCheckbox.prop('disabled', false);
}
}
},
showNotification: function(options) {
if (this.notification) {
this.notification.remove();
}
this.notification = new NotificationView({
model: new NotificationModel(options)
});
// It's ugly to reach outside to the cohort-management div, but we want this notification
// message to always be visible (as opposed to using the transient notification area defined
// by cohorts.js).
$('.cohort-management').before(this.notification.$el);
this.notification.render();
}
});
return VerifiedTrackSettingsNotificationView;
});
}).call(this, define || RequireJS.define);

View File

@@ -15,13 +15,13 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
dogLoversInitialCount = 456,
unknownUserMessage,
invalidEmailMessage, createMockCohort, createMockCohorts, createMockContentGroups,
createMockCohortSettingsJson, createMockVerifiedTrackCohortsJson, flushVerifiedTrackCohortRequests,
createMockCohortSettingsJson,
createCohortsView, cohortsView, requests, respondToRefresh, verifyMessage, verifyNoMessage,
verifyDetailedMessage, verifyHeader, verifyVerifiedTrackMessage, verifyVerifiedTrackUIUpdates,
verifyDetailedMessage, verifyHeader,
expectCohortAddRequest, getAddModal, selectContentGroup, clearContentGroup,
saveFormAndExpectErrors, createMockCohortSettings, MOCK_COHORTED_USER_PARTITION_ID,
MOCK_UPLOAD_COHORTS_CSV_URL, MOCK_STUDIO_ADVANCED_SETTINGS_URL, MOCK_STUDIO_GROUP_CONFIGURATIONS_URL,
MOCK_VERIFIED_TRACK_COHORTING_URL, MOCK_MANUAL_ASSIGNMENT, MOCK_RANDOM_ASSIGNMENT;
MOCK_MANUAL_ASSIGNMENT, MOCK_RANDOM_ASSIGNMENT;
MOCK_MANUAL_ASSIGNMENT = 'manual';
MOCK_RANDOM_ASSIGNMENT = 'random';
@@ -29,7 +29,6 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
MOCK_UPLOAD_COHORTS_CSV_URL = 'http://upload-csv-file-url/';
MOCK_STUDIO_ADVANCED_SETTINGS_URL = 'http://studio/settings/advanced';
MOCK_STUDIO_GROUP_CONFIGURATIONS_URL = 'http://studio/group_configurations';
MOCK_VERIFIED_TRACK_COHORTING_URL = 'http://courses/foo/verified_track_content/settings';
createMockCohort = function(name, id, userCount, groupId, userPartitionId, assignmentType) {
return {
@@ -75,17 +74,6 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
);
};
createMockVerifiedTrackCohortsJson = function(enabled) {
if (enabled) {
return {
enabled: true,
verified_cohort_name: 'Verified Track'
};
} else {
return {enabled: false};
}
};
createCohortsView = function(test, options) {
var cohortsJson, cohorts, contentGroups, cohortSettings;
options = options || {};
@@ -105,7 +93,6 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
uploadCohortsCsvUrl: MOCK_UPLOAD_COHORTS_CSV_URL,
studioAdvancedSettingsUrl: MOCK_STUDIO_ADVANCED_SETTINGS_URL,
studioGroupConfigurationsUrl: MOCK_STUDIO_GROUP_CONFIGURATIONS_URL,
verifiedTrackCohortingUrl: MOCK_VERIFIED_TRACK_COHORTING_URL
}
});
@@ -114,20 +101,6 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
cohortsView.$('.cohort-select').val(options.selectCohort.toString()).change();
}
flushVerifiedTrackCohortRequests(options.enableVerifiedTrackCohorting);
};
// Flush out all requests to get verified track cohort information.
// The order relative to other requests is not important to encode,
// and for pre-existing test cases, we don't care about these additional requests.
flushVerifiedTrackCohortRequests = function(enableVerifiedTrackCohorting) {
for (var i = requests.length - 1; i >= 0; i--) {
if (requests[i].url === MOCK_VERIFIED_TRACK_COHORTING_URL) {
AjaxHelpers.respondWithJson(
requests, createMockVerifiedTrackCohortsJson(enableVerifiedTrackCohorting), i
);
}
}
};
respondToRefresh = function(catCount, dogCount) {
@@ -181,26 +154,6 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
expect(cohortsView.$('.message').length).toBe(0);
};
verifyVerifiedTrackMessage = function(type, expectedText) {
if (type) {
expect($('.message').length).toBe(1);
expect($('.message-' + type).length).toBe(1);
expect($('.message-title').text()).toContain(expectedText);
} else {
expect($('.message').length).toBe(0);
}
};
verifyVerifiedTrackUIUpdates = function(enableCohortsCheckbox, disableCohortNameField) {
expect(cohortsView.$('.cohorts-state').prop('disabled')).toBe(enableCohortsCheckbox);
// Select settings tab
if (disableCohortNameField !== undefined) {
cohortsView.$('.cohort-select').val('1').change();
cohortsView.$('.tab-settings a').click();
expect(cohortsView.$('.cohort-name').prop('readonly')).toBe(disableCohortNameField);
}
};
verifyDetailedMessage = function(expectedTitle, expectedMessageType, expectedDetails, expectedAction) {
var numDetails = cohortsView.$('.summary-items').children().length;
verifyMessage(expectedTitle, expectedMessageType, expectedAction, true);
@@ -341,53 +294,6 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
});
});
describe('Verified Track Cohorting Settings View', function() {
it('displays no message if the feature is disabled', function() {
createCohortsView(this);
verifyVerifiedTrackMessage(false);
verifyVerifiedTrackUIUpdates(false, false);
});
it('displays a confirmation if the feature is enabled and a verified track cohort exists', function() {
var cohortName = 'Verified Track';
createCohortsView(this, {
cohorts: [
{
id: 1,
name: cohortName,
assignment_type: MOCK_MANUAL_ASSIGNMENT,
group_id: 111,
user_partition_id: MOCK_COHORTED_USER_PARTITION_ID
}
],
enableVerifiedTrackCohorting: true
});
verifyVerifiedTrackMessage(
'confirmation', 'automatic cohorting for verified track learners. You cannot disable cohorts'
);
verifyVerifiedTrackUIUpdates(true, true);
});
it('displays an error if no verified track cohort exists', function() {
createCohortsView(this, {enableVerifiedTrackCohorting: true});
verifyVerifiedTrackMessage(
'error', 'cohorting enabled for verified track learners, but the required cohort does not exist'
);
verifyVerifiedTrackUIUpdates(true, false);
});
it('displays an error if cohorting is disabled', function() {
createCohortsView(this, {
cohortSettings: createMockCohortSettings(false),
enableVerifiedTrackCohorting: true
});
verifyVerifiedTrackMessage(
'error', 'automatic cohorting enabled for verified track learners, but cohorts are disabled'
);
verifyVerifiedTrackUIUpdates(false);
});
});
describe('Course Cohort Settings', function() {
it('can enable and disable cohorting', function() {
createCohortsView(this, {cohortSettings: createMockCohortSettings(false)});

View File

@@ -24,13 +24,15 @@
<%- gettext('Cohort Name') %> *
<span class="sr"><%- gettext('(Required Field)')%></span>
</label>
<input name="cohort-name" value="<%- cohort_name_value %>" class="input cohort-name
<% if (cohort.disableEditingName) { %>
readonly" readonly
<% } else { %>
"
<% } %>
id="cohort-name" placeholder="<%- placeholder_value %>" required="required" type="text">
<input
name="cohort-name"
value="<%- cohort_name_value %>"
class="input cohort-name"
id="cohort-name"
placeholder="<%- placeholder_value %>"
required="required"
type="text"
>
</div>
<hr class="divider divider-lv1">
<% if (isDefaultCohort) { %>

View File

@@ -13,7 +13,6 @@ from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_
data-cohorts_url="${section_data['cohorts_url']}"
data-upload_cohorts_csv_url="${section_data['upload_cohorts_csv_url']}"
data-course_cohort_settings_url="${section_data['course_cohort_settings_url']}"
data-verified_track_cohorting_url="${section_data['verified_track_cohorting_url']}"
data-is_ccx_enabled="${'true' if section_data['ccx_is_enabled'] else 'false'}"
>
</div>

View File

@@ -49,7 +49,6 @@ from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_authn.views.login import redirect_to_lms_login
from openedx.core.djangoapps.verified_track_content import views as verified_track_content_views
from openedx.features.enterprise_support.api import enterprise_enabled
from common.djangoapps.student import views as student_views
from common.djangoapps.util import views as util_views
@@ -607,13 +606,6 @@ urlpatterns += [
discussion_views.discussion_topics,
name='discussion_topics',
),
re_path(
r'^courses/{}/verified_track_content/settings'.format(
settings.COURSE_KEY_PATTERN,
),
verified_track_content_views.cohorting_settings,
name='verified_track_cohorting',
),
# LTI endpoints listing
re_path(

View File

@@ -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"""

View File

@@ -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

View File

@@ -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()

View File

@@ -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()),
],
),
]

View File

@@ -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),
),
]

View File

@@ -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')),
],
),
]

View File

@@ -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]

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)