refactor: remove VerifiedTrackCohortedCourse feature
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
@@ -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() {
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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)});
|
||||
|
||||
@@ -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) { %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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