Enable/disable cohorts from the instructor dashboard and move cohorts management to its own tab
TNL-1268
This commit is contained in:
committed by
Usman Khalid
parent
d839692437
commit
3ce494f5c5
@@ -28,6 +28,15 @@ class InstructorDashboardPage(CoursePage):
|
||||
membership_section.wait_for_page()
|
||||
return membership_section
|
||||
|
||||
def select_cohort_management(self):
|
||||
"""
|
||||
Selects the cohort management tab and returns the CohortManagementSection
|
||||
"""
|
||||
self.q(css='a[data-section=cohort_management]').first.click()
|
||||
cohort_management_section = CohortManagementSection(self.browser)
|
||||
cohort_management_section.wait_for_page()
|
||||
return cohort_management_section
|
||||
|
||||
def select_data_download(self):
|
||||
"""
|
||||
Selects the data download tab and returns a DataDownloadPage.
|
||||
@@ -84,16 +93,10 @@ class MembershipPage(PageObject):
|
||||
"""
|
||||
return MembershipPageAutoEnrollSection(self.browser)
|
||||
|
||||
def select_cohort_management_section(self):
|
||||
"""
|
||||
Returns the MembershipPageCohortManagementSection page object.
|
||||
"""
|
||||
return MembershipPageCohortManagementSection(self.browser)
|
||||
|
||||
|
||||
class MembershipPageCohortManagementSection(PageObject):
|
||||
class CohortManagementSection(PageObject):
|
||||
"""
|
||||
The cohort management subsection of the Membership section of the Instructor dashboard.
|
||||
The Cohort Management section of the Instructor dashboard.
|
||||
"""
|
||||
url = None
|
||||
csv_browse_button_selector_css = '.csv-upload #file-upload-form-file'
|
||||
@@ -104,13 +107,13 @@ class MembershipPageCohortManagementSection(PageObject):
|
||||
assignment_type_buttons_css = '.cohort-management-assignment-type-settings input'
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css='.cohort-management.membership-section').present
|
||||
return self.q(css='.cohort-management').present
|
||||
|
||||
def _bounded_selector(self, selector):
|
||||
"""
|
||||
Return `selector`, but limited to the cohort management context.
|
||||
"""
|
||||
return '.cohort-management.membership-section {}'.format(selector)
|
||||
return '.cohort-management {}'.format(selector)
|
||||
|
||||
def _get_cohort_options(self):
|
||||
"""
|
||||
@@ -158,10 +161,10 @@ class MembershipPageCohortManagementSection(PageObject):
|
||||
Return assignment settings disabled message in case of default cohort.
|
||||
"""
|
||||
query = self.q(css=self._bounded_selector('.copy-error'))
|
||||
if query.present:
|
||||
if query.visible:
|
||||
return query.text[0]
|
||||
else:
|
||||
return ''
|
||||
|
||||
return ''
|
||||
|
||||
@property
|
||||
def cohort_name_in_header(self):
|
||||
@@ -232,7 +235,11 @@ class MembershipPageCohortManagementSection(PageObject):
|
||||
Adds a new manual cohort with the specified name.
|
||||
If a content group should also be associated, the name of the content group should be specified.
|
||||
"""
|
||||
create_buttons = self.q(css=self._bounded_selector(".action-create"))
|
||||
add_cohort_selector = self._bounded_selector(".action-create")
|
||||
|
||||
# We need to wait because sometime add cohort button is not in a state to be clickable.
|
||||
self.wait_for_element_presence(add_cohort_selector, 'Add Cohort button is present.')
|
||||
create_buttons = self.q(css=add_cohort_selector)
|
||||
# There are 2 create buttons on the page. The second one is only present when no cohort yet exists
|
||||
# (in which case the first is not visible). Click on the last present create button.
|
||||
create_buttons.results[len(create_buttons.results) - 1].click()
|
||||
@@ -444,6 +451,28 @@ class MembershipPageCohortManagementSection(PageObject):
|
||||
file_input.send_keys(path)
|
||||
self.q(css=self._bounded_selector(self.csv_upload_button_selector_css)).first.click()
|
||||
|
||||
@property
|
||||
def is_cohorted(self):
|
||||
"""
|
||||
Returns the state of `Enable Cohorts` checkbox state.
|
||||
"""
|
||||
return self.q(css=self._bounded_selector('.cohorts-state')).selected
|
||||
|
||||
@is_cohorted.setter
|
||||
def is_cohorted(self, state):
|
||||
"""
|
||||
Check/Uncheck the `Enable Cohorts` checkbox state.
|
||||
"""
|
||||
if state != self.is_cohorted:
|
||||
self.q(css=self._bounded_selector('.cohorts-state')).first.click()
|
||||
|
||||
def cohort_management_controls_visible(self):
|
||||
"""
|
||||
Return the visibility status of cohort management controls(cohort selector section etc).
|
||||
"""
|
||||
return (self.q(css=self._bounded_selector('.cohort-management-nav')).visible and
|
||||
self.q(css=self._bounded_selector('.wrapper-cohort-supplemental')).visible)
|
||||
|
||||
|
||||
class MembershipPageAutoEnrollSection(PageObject):
|
||||
"""
|
||||
|
||||
@@ -63,8 +63,7 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin
|
||||
# go to the membership page on the instructor dashboard
|
||||
self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
|
||||
self.instructor_dashboard_page.visit()
|
||||
membership_page = self.instructor_dashboard_page.select_membership()
|
||||
self.cohort_management_page = membership_page.select_cohort_management_section()
|
||||
self.cohort_management_page = self.instructor_dashboard_page.select_cohort_management()
|
||||
|
||||
def verify_cohort_description(self, cohort_name, expected_description):
|
||||
"""
|
||||
@@ -441,9 +440,31 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin
|
||||
|
||||
self.assertTrue(self.cohort_management_page.is_assignment_settings_disabled)
|
||||
|
||||
message = "There must be one cohort to which students can be randomly assigned."
|
||||
message = "There must be one cohort to which students can automatically be assigned."
|
||||
self.assertEqual(message, self.cohort_management_page.assignment_settings_message)
|
||||
|
||||
def test_cohort_enable_disable(self):
|
||||
"""
|
||||
Scenario: Cohort Enable/Disable checkbox related functionality is working as intended.
|
||||
|
||||
Given I have a cohorted course with a user.
|
||||
And I can see the `Enable Cohorts` checkbox is checked.
|
||||
And cohort management controls are visible.
|
||||
When I uncheck the `Enable Cohorts` checkbox.
|
||||
Then I cohort management controls are not visible.
|
||||
And When I reload the page.
|
||||
Then I can see the `Enable Cohorts` checkbox is unchecked.
|
||||
And cohort management controls are not visible.
|
||||
"""
|
||||
self.assertTrue(self.cohort_management_page.is_cohorted)
|
||||
self.assertTrue(self.cohort_management_page.cohort_management_controls_visible())
|
||||
self.cohort_management_page.is_cohorted = False
|
||||
self.assertFalse(self.cohort_management_page.cohort_management_controls_visible())
|
||||
self.browser.refresh()
|
||||
self.cohort_management_page.wait_for_page()
|
||||
self.assertFalse(self.cohort_management_page.is_cohorted)
|
||||
self.assertFalse(self.cohort_management_page.cohort_management_controls_visible())
|
||||
|
||||
def test_link_to_data_download(self):
|
||||
"""
|
||||
Scenario: a link is present from the cohort configuration in
|
||||
@@ -656,8 +677,7 @@ class CohortContentGroupAssociationTest(UniqueCourseTest, CohortTestMixin):
|
||||
# go to the membership page on the instructor dashboard
|
||||
self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
|
||||
self.instructor_dashboard_page.visit()
|
||||
membership_page = self.instructor_dashboard_page.select_membership()
|
||||
self.cohort_management_page = membership_page.select_cohort_management_section()
|
||||
self.cohort_management_page = self.instructor_dashboard_page.select_cohort_management()
|
||||
|
||||
def test_no_content_group_linked(self):
|
||||
"""
|
||||
|
||||
@@ -154,8 +154,7 @@ class EndToEndCohortedCoursewareTest(ContainerBase):
|
||||
"""
|
||||
instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
|
||||
instructor_dashboard_page.visit()
|
||||
membership_page = instructor_dashboard_page.select_membership()
|
||||
cohort_management_page = membership_page.select_cohort_management_section()
|
||||
cohort_management_page = instructor_dashboard_page.select_cohort_management()
|
||||
|
||||
def add_cohort_with_student(cohort_name, content_group, student):
|
||||
cohort_management_page.add_cohort(cohort_name, content_group=content_group)
|
||||
|
||||
@@ -65,9 +65,7 @@ def instructor_dashboard_2(request, course_id):
|
||||
'finance_admin': CourseFinanceAdminRole(course_key).has_user(request.user),
|
||||
'sales_admin': CourseSalesAdminRole(course_key).has_user(request.user),
|
||||
'staff': has_access(request.user, 'staff', course),
|
||||
'forum_admin': has_forum_access(
|
||||
request.user, course_key, FORUM_ROLE_ADMINISTRATOR
|
||||
),
|
||||
'forum_admin': has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR),
|
||||
}
|
||||
|
||||
if not access['staff']:
|
||||
@@ -79,6 +77,7 @@ def instructor_dashboard_2(request, course_id):
|
||||
_section_student_admin(course, access),
|
||||
_section_data_download(course, access),
|
||||
_section_analytics(course, access),
|
||||
_section_cohort_management(course, access),
|
||||
]
|
||||
|
||||
#check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course
|
||||
@@ -330,7 +329,22 @@ def _section_membership(course, access):
|
||||
'modify_access_url': reverse('modify_access', kwargs={'course_id': unicode(course_key)}),
|
||||
'list_forum_members_url': reverse('list_forum_members', kwargs={'course_id': unicode(course_key)}),
|
||||
'update_forum_role_membership_url': reverse('update_forum_role_membership', kwargs={'course_id': unicode(course_key)}),
|
||||
'cohorts_ajax_url': reverse('cohorts', kwargs={'course_key_string': unicode(course_key)}),
|
||||
}
|
||||
return section_data
|
||||
|
||||
|
||||
def _section_cohort_management(course, access):
|
||||
""" Provide data for the corresponding cohort management section """
|
||||
course_key = course.id
|
||||
section_data = {
|
||||
'section_key': 'cohort_management',
|
||||
'section_display_name': _('Cohort Management'),
|
||||
'access': access,
|
||||
'course_cohort_settings_url': reverse(
|
||||
'course_cohort_settings',
|
||||
kwargs={'course_key_string': unicode(course_key)}
|
||||
),
|
||||
'cohorts_url': reverse('cohorts', kwargs={'course_key_string': unicode(course_key)}),
|
||||
'advanced_settings_url': get_studio_url(course, 'settings/advanced'),
|
||||
'upload_cohorts_csv_url': reverse('add_users_to_cohorts', kwargs={'course_id': unicode(course_key)}),
|
||||
}
|
||||
|
||||
@@ -176,6 +176,9 @@ setup_instructor_dashboard_sections = (idash_content) ->
|
||||
,
|
||||
constructor: window.InstructorDashboard.sections.Metrics
|
||||
$element: idash_content.find ".#{CSS_IDASH_SECTION}#metrics"
|
||||
,
|
||||
constructor: window.InstructorDashboard.sections.CohortManagement
|
||||
$element: idash_content.find ".#{CSS_IDASH_SECTION}#cohort_management"
|
||||
]
|
||||
|
||||
sections_to_initialize.map ({constructor, $element}) ->
|
||||
|
||||
35
lms/static/js/factories/cohorts_factory.js
Normal file
35
lms/static/js/factories/cohorts_factory.js
Normal file
@@ -0,0 +1,35 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define(['jquery', 'js/groups/views/cohorts', 'js/groups/collections/cohort', 'js/groups/models/course_cohort_settings'],
|
||||
function($) {
|
||||
|
||||
return function(contentGroups, studioGroupConfigurationsUrl) {
|
||||
|
||||
var cohorts = new edx.groups.CohortCollection(),
|
||||
courseCohortSettings = new edx.groups.CourseCohortSettingsModel();
|
||||
|
||||
var cohortManagementElement = $('.cohort-management');
|
||||
|
||||
cohorts.url = cohortManagementElement.data('cohorts_url');
|
||||
courseCohortSettings.url = cohortManagementElement.data('course_cohort_settings_url');
|
||||
|
||||
var cohortsView = new edx.groups.CohortsView({
|
||||
el: cohortManagementElement,
|
||||
model: cohorts,
|
||||
contentGroups: contentGroups,
|
||||
cohortSettings: courseCohortSettings,
|
||||
context: {
|
||||
uploadCohortsCsvUrl: cohortManagementElement.data('upload_cohorts_csv_url'),
|
||||
studioAdvancedSettingsUrl: cohortManagementElement.data('advanced-settings-url'),
|
||||
studioGroupConfigurationsUrl: studioGroupConfigurationsUrl
|
||||
}
|
||||
});
|
||||
cohorts.fetch().done(function() {
|
||||
courseCohortSettings.fetch().done(function() {
|
||||
cohortsView.render();
|
||||
})
|
||||
});
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
|
||||
16
lms/static/js/groups/models/course_cohort_settings.js
Normal file
16
lms/static/js/groups/models/course_cohort_settings.js
Normal file
@@ -0,0 +1,16 @@
|
||||
var edx = edx || {};
|
||||
|
||||
(function(Backbone) {
|
||||
'use strict';
|
||||
|
||||
edx.groups = edx.groups || {};
|
||||
|
||||
edx.groups.CourseCohortSettingsModel = Backbone.Model.extend({
|
||||
idAttribute: 'id',
|
||||
defaults: {
|
||||
is_cohorted: false,
|
||||
cohorted_discussions: [],
|
||||
always_cohort_inline_discussions: true
|
||||
}
|
||||
});
|
||||
}).call(this, Backbone);
|
||||
@@ -1,7 +1,7 @@
|
||||
var edx = edx || {};
|
||||
|
||||
(function($, _, Backbone, gettext, interpolate_text, CohortModel, CohortEditorView, CohortFormView,
|
||||
NotificationModel, NotificationView, FileUploaderView) {
|
||||
CourseCohortSettingsNotificationView, NotificationModel, NotificationView, FileUploaderView) {
|
||||
'use strict';
|
||||
|
||||
var hiddenClass = 'is-hidden',
|
||||
@@ -12,6 +12,7 @@ var edx = edx || {};
|
||||
edx.groups.CohortsView = Backbone.View.extend({
|
||||
events : {
|
||||
'change .cohort-select': 'onCohortSelected',
|
||||
'change .cohorts-state': 'onCohortsEnabledChanged',
|
||||
'click .action-create': 'showAddCohortForm',
|
||||
'click .cohort-management-add-form .action-save': 'saveAddCohortForm',
|
||||
'click .cohort-management-add-form .action-cancel': 'cancelAddCohortForm',
|
||||
@@ -26,19 +27,21 @@ var edx = edx || {};
|
||||
this.selectorTemplate = _.template($('#cohort-selector-tpl').text());
|
||||
this.context = options.context;
|
||||
this.contentGroups = options.contentGroups;
|
||||
this.cohortSettings = options.cohortSettings;
|
||||
model.on('sync', this.onSync, this);
|
||||
|
||||
// Update cohort counts when the user clicks back on the membership tab
|
||||
// Update cohort counts when the user clicks back on the cohort management tab
|
||||
// (for example, after uploading a csv file of cohort assignments and then
|
||||
// checking results on data download tab).
|
||||
$(this.getSectionCss('membership')).click(function () {
|
||||
$(this.getSectionCss('cohort_management')).click(function () {
|
||||
model.fetch();
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.html(this.template({
|
||||
cohorts: this.model.models
|
||||
cohorts: this.model.models,
|
||||
cohortsEnabled: this.cohortSettings.get('is_cohorted')
|
||||
}));
|
||||
this.onSync();
|
||||
return this;
|
||||
@@ -51,6 +54,13 @@ var edx = edx || {};
|
||||
}));
|
||||
},
|
||||
|
||||
renderCourseCohortSettingsNotificationView: function() {
|
||||
var cohortStateMessageNotificationView = new CourseCohortSettingsNotificationView({
|
||||
el: $('.cohort-state-message'),
|
||||
cohortEnabled: this.getCohortsEnabled()});
|
||||
cohortStateMessageNotificationView.render();
|
||||
},
|
||||
|
||||
onSync: function(model, response, options) {
|
||||
var selectedCohort = this.lastSelectedCohortId && this.model.get(this.lastSelectedCohortId),
|
||||
hasCohorts = this.model.length > 0,
|
||||
@@ -98,6 +108,28 @@ var edx = edx || {};
|
||||
this.showCohortEditor(selectedCohort);
|
||||
},
|
||||
|
||||
onCohortsEnabledChanged: function(event) {
|
||||
event.preventDefault();
|
||||
this.saveCohortSettings();
|
||||
},
|
||||
|
||||
saveCohortSettings: function() {
|
||||
var self = this,
|
||||
cohortSettings,
|
||||
fieldData = {is_cohorted: this.getCohortsEnabled()};
|
||||
cohortSettings = this.cohortSettings;
|
||||
cohortSettings.save(
|
||||
fieldData, {wait: true}
|
||||
).done(function() {
|
||||
self.render();
|
||||
self.renderCourseCohortSettingsNotificationView();
|
||||
});
|
||||
},
|
||||
|
||||
getCohortsEnabled: function() {
|
||||
return this.$('.cohorts-state').prop('checked');
|
||||
},
|
||||
|
||||
showCohortEditor: function(cohort) {
|
||||
this.removeNotification();
|
||||
if (this.editor) {
|
||||
@@ -242,4 +274,5 @@ var edx = edx || {};
|
||||
}
|
||||
});
|
||||
}).call(this, $, _, Backbone, gettext, interpolate_text, edx.groups.CohortModel, edx.groups.CohortEditorView,
|
||||
edx.groups.CohortFormView, NotificationModel, NotificationView, FileUploaderView);
|
||||
edx.groups.CohortFormView, edx.groups.CourseCohortSettingsNotificationView, NotificationModel, NotificationView,
|
||||
FileUploaderView);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
var edx = edx || {};
|
||||
|
||||
(function($, _, Backbone, gettext) {
|
||||
'use strict';
|
||||
|
||||
edx.groups = edx.groups || {};
|
||||
|
||||
edx.groups.CourseCohortSettingsNotificationView = Backbone.View.extend({
|
||||
initialize: function(options) {
|
||||
this.template = _.template($('#cohort-state-tpl').text());
|
||||
this.cohortEnabled = options.cohortEnabled;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.html(this.template({}));
|
||||
this.showCohortStateMessage();
|
||||
return this;
|
||||
},
|
||||
|
||||
showCohortStateMessage: function () {
|
||||
var actionToggleMessage = this.$('.action-toggle-message');
|
||||
|
||||
// The following lines are necessary to re-trigger the CSS animation on span.action-toggle-message
|
||||
actionToggleMessage.removeClass('is-fleeting');
|
||||
actionToggleMessage.offset().width = actionToggleMessage.offset().width;
|
||||
actionToggleMessage.addClass('is-fleeting');
|
||||
|
||||
if (this.cohortEnabled) {
|
||||
actionToggleMessage.text(gettext('Cohorts Enabled'));
|
||||
} else {
|
||||
actionToggleMessage.text(gettext('Cohorts Disabled'));
|
||||
}
|
||||
}
|
||||
});
|
||||
}).call(this, $, _, Backbone, gettext);
|
||||
29
lms/static/js/instructor_dashboard/cohort_management.js
Normal file
29
lms/static/js/instructor_dashboard/cohort_management.js
Normal file
@@ -0,0 +1,29 @@
|
||||
(function() {
|
||||
var CohortManagement;
|
||||
|
||||
CohortManagement = (function() {
|
||||
|
||||
function CohortManagement($section) {
|
||||
this.$section = $section;
|
||||
this.$section.data('wrapper', this);
|
||||
}
|
||||
|
||||
CohortManagement.prototype.onClickTitle = function() {};
|
||||
|
||||
return CohortManagement;
|
||||
|
||||
})();
|
||||
|
||||
_.defaults(window, {
|
||||
InstructorDashboard: {}
|
||||
});
|
||||
|
||||
_.defaults(window.InstructorDashboard, {
|
||||
sections: {}
|
||||
});
|
||||
|
||||
_.defaults(window.InstructorDashboard.sections, {
|
||||
CohortManagement: CohortManagement
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -1,15 +1,18 @@
|
||||
define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
|
||||
'js/groups/views/cohorts', 'js/groups/collections/cohort', 'js/groups/models/content_group'],
|
||||
function (Backbone, $, AjaxHelpers, TemplateHelpers, CohortsView, CohortCollection, ContentGroupModel) {
|
||||
'js/groups/views/cohorts', 'js/groups/collections/cohort', 'js/groups/models/content_group',
|
||||
'js/groups/models/course_cohort_settings', 'js/groups/views/course_cohort_settings_notification'],
|
||||
function (Backbone, $, AjaxHelpers, TemplateHelpers, CohortsView, CohortCollection, ContentGroupModel,
|
||||
CourseCohortSettingsModel, CourseCohortSettingsNotificationView) {
|
||||
'use strict';
|
||||
|
||||
describe("Cohorts View", function () {
|
||||
var catLoversInitialCount = 123, dogLoversInitialCount = 456, unknownUserMessage,
|
||||
createMockCohort, createMockCohorts, createMockContentGroups, createCohortsView, cohortsView,
|
||||
requests, respondToRefresh, verifyMessage, verifyNoMessage, verifyDetailedMessage, verifyHeader,
|
||||
expectCohortAddRequest, getAddModal, selectContentGroup, clearContentGroup, saveFormAndExpectErrors,
|
||||
MOCK_COHORTED_USER_PARTITION_ID, MOCK_UPLOAD_COHORTS_CSV_URL, MOCK_STUDIO_ADVANCED_SETTINGS_URL,
|
||||
MOCK_STUDIO_GROUP_CONFIGURATIONS_URL, MOCK_MANUAL_ASSIGNMENT, MOCK_RANDOM_ASSIGNMENT;
|
||||
createMockCohort, createMockCohorts, createMockContentGroups, createCohortSettings, createCohortsView,
|
||||
cohortsView, requests, respondToRefresh, verifyMessage, verifyNoMessage, 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_MANUAL_ASSIGNMENT, MOCK_RANDOM_ASSIGNMENT;
|
||||
|
||||
MOCK_MANUAL_ASSIGNMENT = 'manual';
|
||||
MOCK_RANDOM_ASSIGNMENT = 'random';
|
||||
@@ -49,17 +52,35 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
|
||||
];
|
||||
};
|
||||
|
||||
createMockCohortSettings = function (isCohorted, cohortedDiscussions, alwaysCohortInlineDiscussions) {
|
||||
return {
|
||||
id: 0,
|
||||
is_cohorted: isCohorted || false,
|
||||
cohorted_discussions: cohortedDiscussions || [],
|
||||
always_cohort_inline_discussions: alwaysCohortInlineDiscussions || true
|
||||
};
|
||||
};
|
||||
|
||||
createCohortSettings = function (isCohorted, cohortedDiscussions, alwaysCohortInlineDiscussions) {
|
||||
return new CourseCohortSettingsModel(
|
||||
createMockCohortSettings(isCohorted, cohortedDiscussions, alwaysCohortInlineDiscussions)
|
||||
);
|
||||
};
|
||||
|
||||
createCohortsView = function (test, options) {
|
||||
var cohortsJson, cohorts, contentGroups;
|
||||
var cohortsJson, cohorts, contentGroups, cohortSettings;
|
||||
options = options || {};
|
||||
cohortsJson = options.cohorts ? {cohorts: options.cohorts} : createMockCohorts();
|
||||
cohorts = new CohortCollection(cohortsJson, {parse: true});
|
||||
contentGroups = options.contentGroups || createMockContentGroups();
|
||||
cohortSettings = options.cohortSettings || createCohortSettings(true);
|
||||
cohortSettings.url = '/mock_service/cohorts/settings';
|
||||
cohorts.url = '/mock_service/cohorts';
|
||||
requests = AjaxHelpers.requests(test);
|
||||
cohortsView = new CohortsView({
|
||||
model: cohorts,
|
||||
contentGroups: contentGroups,
|
||||
cohortSettings: cohortSettings,
|
||||
context: {
|
||||
uploadCohortsCsvUrl: MOCK_UPLOAD_COHORTS_CSV_URL,
|
||||
studioAdvancedSettingsUrl: MOCK_STUDIO_ADVANCED_SETTINGS_URL,
|
||||
@@ -177,13 +198,14 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
setFixtures('<ul class="instructor-nav"><li class="nav-item"><<a href data-section="membership" class="active-section">Membership</a></li></ul><div></div>');
|
||||
setFixtures('<ul class="instructor-nav"><li class="nav-item"><<a href data-section="cohort_management" class="active-section">Cohort Management</a></li></ul><div></div><div class="cohort-state-message"></div>');
|
||||
TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohorts');
|
||||
TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-form');
|
||||
TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-selector');
|
||||
TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-editor');
|
||||
TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-group-header');
|
||||
TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/notification');
|
||||
TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-state');
|
||||
TemplateHelpers.installTemplate('templates/file-upload');
|
||||
});
|
||||
|
||||
@@ -202,7 +224,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
|
||||
it("syncs data when membership tab is clicked", function() {
|
||||
createCohortsView(this, {selectCohort: 1});
|
||||
verifyHeader(1, 'Cat Lovers', catLoversInitialCount);
|
||||
$(cohortsView.getSectionCss("membership")).click();
|
||||
$(cohortsView.getSectionCss("cohort_management")).click();
|
||||
AjaxHelpers.expectRequest(requests, 'GET', '/mock_service/cohorts');
|
||||
respondToRefresh(1001, 2);
|
||||
verifyHeader(1, 'Cat Lovers', 1001);
|
||||
@@ -255,6 +277,54 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
|
||||
});
|
||||
});
|
||||
|
||||
describe("Course Cohort Settings", function () {
|
||||
it('enable/disable working correctly', function () {
|
||||
createCohortsView(this, {cohortSettings: createCohortSettings(false)});
|
||||
|
||||
expect(cohortsView.$('.cohorts-state').prop('checked')).toBeFalsy();
|
||||
|
||||
cohortsView.$('.cohorts-state').prop('checked', true).change();
|
||||
AjaxHelpers.expectJsonRequest(
|
||||
requests, 'PUT', '/mock_service/cohorts/settings',
|
||||
createMockCohortSettings(true, [], true)
|
||||
);
|
||||
AjaxHelpers.respondWithJson(
|
||||
requests,
|
||||
createMockCohortSettings(true)
|
||||
);
|
||||
expect(cohortsView.$('.cohorts-state').prop('checked')).toBeTruthy();
|
||||
|
||||
cohortsView.$('.cohorts-state').prop('checked', false).change();
|
||||
AjaxHelpers.expectJsonRequest(
|
||||
requests, 'PUT', '/mock_service/cohorts/settings',
|
||||
createMockCohortSettings(false, [], true)
|
||||
);
|
||||
AjaxHelpers.respondWithJson(
|
||||
requests,
|
||||
createMockCohortSettings(false)
|
||||
);
|
||||
expect(cohortsView.$('.cohorts-state').prop('checked')).toBeFalsy();
|
||||
});
|
||||
|
||||
|
||||
it('Course Cohort Settings Notification View renders correctly', function () {
|
||||
var createCourseCohortSettingsNotificationView = function (is_cohorted) {
|
||||
var notificationView = new CourseCohortSettingsNotificationView({
|
||||
el: $('.cohort-state-message'),
|
||||
cohortEnabled: is_cohorted});
|
||||
notificationView.render();
|
||||
return notificationView;
|
||||
};
|
||||
|
||||
var notificationView = createCourseCohortSettingsNotificationView(true);
|
||||
expect(notificationView.$('.action-toggle-message').text().trim()).toBe('Cohorts Enabled');
|
||||
|
||||
notificationView = createCourseCohortSettingsNotificationView(false);
|
||||
expect(notificationView.$('.action-toggle-message').text().trim()).toBe('Cohorts Disabled');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Cohort Group Header", function () {
|
||||
it("renders header correctly", function () {
|
||||
var cohortName = 'Transformers',
|
||||
@@ -867,7 +937,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
|
||||
|
||||
// We have a single random cohort so we should not be allowed to change it assignment type
|
||||
expect(cohortsView.$('.cohort-management-assignment-type-settings')).toHaveClass('is-disabled');
|
||||
expect(cohortsView.$('.copy-error').text()).toContain("There must be one cohort to which students can be randomly assigned.");
|
||||
expect(cohortsView.$('.copy-error').text()).toContain("There must be one cohort to which students can automatically be assigned.");
|
||||
});
|
||||
|
||||
it("cancel settings works", function() {
|
||||
|
||||
@@ -67,6 +67,8 @@
|
||||
'js/views/notification': 'js/views/notification',
|
||||
'js/groups/models/cohort': 'js/groups/models/cohort',
|
||||
'js/groups/models/content_group': 'js/groups/models/content_group',
|
||||
'js/groups/models/course_cohort_settings': 'js/groups/models/course_cohort_settings',
|
||||
'js/groups/views/course_cohort_settings_notification': 'js/groups/views/course_cohort_settings_notification',
|
||||
'js/groups/collections/cohort': 'js/groups/collections/cohort',
|
||||
'js/groups/views/cohort_editor': 'js/groups/views/cohort_editor',
|
||||
'js/groups/views/cohort_form': 'js/groups/views/cohort_form',
|
||||
@@ -294,6 +296,14 @@
|
||||
exports: 'edx.groups.ContentGroupModel',
|
||||
deps: ['backbone']
|
||||
},
|
||||
'js/groups/models/course_cohort_settings': {
|
||||
exports: 'edx.groups.CourseCohortSettingsModel',
|
||||
deps: ['backbone']
|
||||
},
|
||||
'js/groups/views/course_cohort_settings_notification': {
|
||||
exports: 'edx.groups.CourseCohortSettingsNotificationView',
|
||||
deps: ['backbone']
|
||||
},
|
||||
'js/groups/collections/cohort': {
|
||||
exports: 'edx.groups.CohortCollection',
|
||||
deps: ['backbone', 'js/groups/models/cohort']
|
||||
|
||||
@@ -435,6 +435,248 @@
|
||||
}
|
||||
}
|
||||
|
||||
.batch-enrollment, .batch-beta-testers {
|
||||
textarea {
|
||||
margin-top: 0.2em;
|
||||
height: auto;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.request-res-section {
|
||||
margin-top: 1.5em;
|
||||
|
||||
h3 {
|
||||
color: #646464;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
margin-top: 0.5em;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Auto Enroll Csv Section
|
||||
.auto_enroll_csv {
|
||||
.results {
|
||||
|
||||
}
|
||||
.enrollment_signup_button {
|
||||
@include margin-right($baseline/4);
|
||||
}
|
||||
// Custom File upload
|
||||
.customBrowseBtn {
|
||||
margin: ($baseline/2) 0;
|
||||
display: inline-block;
|
||||
.file-browse {
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
display: inline;
|
||||
@include margin-left(-5px);
|
||||
span.browse{
|
||||
@include button(simple, $blue);
|
||||
@include margin-right($baseline);
|
||||
padding: 6px ($baseline/2);
|
||||
font-size: 12px;
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
input.file_field {
|
||||
position:absolute;
|
||||
@include right(0);
|
||||
top:0;
|
||||
margin:0;
|
||||
padding:0;
|
||||
cursor:pointer;
|
||||
opacity:0;
|
||||
filter:alpha(opacity=0);
|
||||
}
|
||||
}
|
||||
& > span, & input[disabled]{
|
||||
vertical-align: middle;
|
||||
}
|
||||
input[disabled] {
|
||||
@include border-radius(4px 0 0 4px);
|
||||
@include padding(6px 6px 5px);
|
||||
border: 1px solid $lightGrey1;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.enroll-option {
|
||||
margin: ($baseline/2) 0;
|
||||
position: relative;
|
||||
|
||||
label {
|
||||
border-bottom: 1px dotted $base-font-color;
|
||||
}
|
||||
|
||||
.hint {
|
||||
@extend %t-copy-sub2;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
@include left($baseline*10);
|
||||
padding: ($baseline/2);
|
||||
width: 50%;
|
||||
background-color: $light-gray;
|
||||
box-shadow: 2px 2px 3px $shadow;
|
||||
|
||||
.hint-caret {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@include left(-15px);
|
||||
@include border-right(8px solid $light-gray);
|
||||
@include border-left(8px solid transparent);
|
||||
border-top: 8px solid $light-gray;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label[for="auto-enroll"]:hover + .auto-enroll-hint {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label[for="auto-enroll-beta"]:hover + .auto-enroll-beta-hint {
|
||||
width: 30%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
label[for="email-students"]:hover + .email-students-hint {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label[for="email-students-beta"]:hover + .email-students-beta-hint {
|
||||
width: 30%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.enroll-actions {
|
||||
margin-top: $baseline;
|
||||
}
|
||||
|
||||
.member-lists-management {
|
||||
|
||||
.wrapper-member-select {
|
||||
padding: ($baseline/2);
|
||||
background-color: $light-gray;
|
||||
}
|
||||
|
||||
.member-lists-selector {
|
||||
display: block;
|
||||
margin: ($baseline/4) 0;
|
||||
padding: ($baseline/4);
|
||||
}
|
||||
|
||||
.auth-list-container {
|
||||
display: none;
|
||||
margin-bottom: ($baseline*1.5);
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.member-list-widget {
|
||||
|
||||
.header {
|
||||
@include box-sizing(border-box);
|
||||
@include border-top-radius(3);
|
||||
position: relative;
|
||||
padding: ($baseline/2);
|
||||
background-color: #efefef;
|
||||
border: 1px solid $light-gray;
|
||||
display: none; // hiding to prefer dropdown as header
|
||||
}
|
||||
|
||||
.title {
|
||||
@include font-size(16);
|
||||
}
|
||||
|
||||
.label,
|
||||
.form-label {
|
||||
@extend %t-copy-sub1;
|
||||
color: $lighter-base-font-color;
|
||||
}
|
||||
|
||||
.info {
|
||||
@include box-sizing(border-box);
|
||||
padding: ($baseline/2);
|
||||
border: 1px solid $light-gray;
|
||||
color: $lighter-base-font-color;
|
||||
line-height: 1.3em;
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
.member-list {
|
||||
@include box-sizing(border-box);
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
thead {
|
||||
background-color: $light-gray;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid $light-gray;
|
||||
}
|
||||
|
||||
td {
|
||||
@extend %t-copy-sub1;
|
||||
vertical-align: middle;
|
||||
padding: ($baseline/2) ($baseline/4);
|
||||
@include border-left(1px solid $light-gray);
|
||||
@include border-right(1px solid $light-gray);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
@include box-sizing(border-box);
|
||||
@include border-bottom-radius(3);
|
||||
position: relative;
|
||||
padding: ($baseline/2);
|
||||
margin-top: -1px;
|
||||
border: 1px solid $light-gray;
|
||||
background-color: #efefef;
|
||||
box-shadow: inset #bbb 0px 1px 1px 0px;
|
||||
}
|
||||
|
||||
// .add-field
|
||||
|
||||
input[type="button"].add {
|
||||
@include idashbutton($blue);
|
||||
position: absolute;
|
||||
@include right($baseline);
|
||||
}
|
||||
}
|
||||
|
||||
.revoke {
|
||||
color: $lighter-base-font-color;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, &:focus {
|
||||
color: $alert-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// view - cohort management
|
||||
// --------------------
|
||||
.instructor-dashboard-wrapper-2 section.idash-section#cohort_management {
|
||||
|
||||
// cohort management
|
||||
%cohort-management-form {
|
||||
|
||||
@@ -733,245 +975,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.batch-enrollment, .batch-beta-testers {
|
||||
textarea {
|
||||
margin-top: 0.2em;
|
||||
height: auto;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.request-res-section {
|
||||
margin-top: 1.5em;
|
||||
|
||||
h3 {
|
||||
color: #646464;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
margin-top: 0.5em;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Auto Enroll Csv Section
|
||||
.auto_enroll_csv {
|
||||
.results {
|
||||
|
||||
}
|
||||
.enrollment_signup_button {
|
||||
@include margin-right($baseline/4);
|
||||
}
|
||||
// Custom File upload
|
||||
.customBrowseBtn {
|
||||
margin: ($baseline/2) 0;
|
||||
display: inline-block;
|
||||
.file-browse {
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
display: inline;
|
||||
@include margin-left(-5px);
|
||||
span.browse{
|
||||
@include button(simple, $blue);
|
||||
@include margin-right($baseline);
|
||||
padding: 6px ($baseline/2);
|
||||
font-size: 12px;
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
input.file_field {
|
||||
position:absolute;
|
||||
@include right(0);
|
||||
top:0;
|
||||
margin:0;
|
||||
padding:0;
|
||||
cursor:pointer;
|
||||
opacity:0;
|
||||
filter:alpha(opacity=0);
|
||||
}
|
||||
}
|
||||
& > span, & input[disabled]{
|
||||
vertical-align: middle;
|
||||
}
|
||||
input[disabled] {
|
||||
@include border-radius(4px 0 0 4px);
|
||||
@include padding(6px 6px 5px);
|
||||
border: 1px solid $lightGrey1;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.enroll-option {
|
||||
margin: ($baseline/2) 0;
|
||||
position: relative;
|
||||
|
||||
label {
|
||||
border-bottom: 1px dotted $base-font-color;
|
||||
}
|
||||
|
||||
.hint {
|
||||
@extend %t-copy-sub2;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
@include left($baseline*10);
|
||||
padding: ($baseline/2);
|
||||
width: 50%;
|
||||
background-color: $light-gray;
|
||||
box-shadow: 2px 2px 3px $shadow;
|
||||
|
||||
.hint-caret {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@include left(-15px);
|
||||
@include border-right(8px solid $light-gray);
|
||||
@include border-left(8px solid transparent);
|
||||
border-top: 8px solid $light-gray;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label[for="auto-enroll"]:hover + .auto-enroll-hint {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label[for="auto-enroll-beta"]:hover + .auto-enroll-beta-hint {
|
||||
width: 30%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
label[for="email-students"]:hover + .email-students-hint {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label[for="email-students-beta"]:hover + .email-students-beta-hint {
|
||||
width: 30%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.enroll-actions {
|
||||
margin-top: $baseline;
|
||||
}
|
||||
|
||||
.member-lists-management {
|
||||
|
||||
.wrapper-member-select {
|
||||
padding: ($baseline/2);
|
||||
background-color: $light-gray;
|
||||
}
|
||||
|
||||
.member-lists-selector {
|
||||
display: block;
|
||||
margin: ($baseline/4) 0;
|
||||
padding: ($baseline/4);
|
||||
}
|
||||
|
||||
.auth-list-container {
|
||||
display: none;
|
||||
margin-bottom: ($baseline*1.5);
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.member-list-widget {
|
||||
|
||||
.header {
|
||||
@include box-sizing(border-box);
|
||||
@include border-top-radius(3);
|
||||
position: relative;
|
||||
padding: ($baseline/2);
|
||||
background-color: #efefef;
|
||||
border: 1px solid $light-gray;
|
||||
display: none; // hiding to prefer dropdown as header
|
||||
}
|
||||
|
||||
.title {
|
||||
@include font-size(16);
|
||||
}
|
||||
|
||||
.label,
|
||||
.form-label {
|
||||
@extend %t-copy-sub1;
|
||||
color: $lighter-base-font-color;
|
||||
}
|
||||
|
||||
.info {
|
||||
@include box-sizing(border-box);
|
||||
padding: ($baseline/2);
|
||||
border: 1px solid $light-gray;
|
||||
color: $lighter-base-font-color;
|
||||
line-height: 1.3em;
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
.member-list {
|
||||
@include box-sizing(border-box);
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
thead {
|
||||
background-color: $light-gray;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid $light-gray;
|
||||
}
|
||||
|
||||
td {
|
||||
@extend %t-copy-sub1;
|
||||
vertical-align: middle;
|
||||
padding: ($baseline/2) ($baseline/4);
|
||||
@include border-left(1px solid $light-gray);
|
||||
@include border-right(1px solid $light-gray);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
@include box-sizing(border-box);
|
||||
@include border-bottom-radius(3);
|
||||
position: relative;
|
||||
padding: ($baseline/2);
|
||||
margin-top: -1px;
|
||||
border: 1px solid $light-gray;
|
||||
background-color: #efefef;
|
||||
box-shadow: inset #bbb 0px 1px 1px 0px;
|
||||
}
|
||||
|
||||
// .add-field
|
||||
|
||||
input[type="button"].add {
|
||||
@include idashbutton($blue);
|
||||
position: absolute;
|
||||
@include right($baseline);
|
||||
}
|
||||
}
|
||||
|
||||
.revoke {
|
||||
color: $lighter-base-font-color;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, &:focus {
|
||||
color: $alert-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.has-other-input-text { // Given to groups which have an 'other' input that appears when needed
|
||||
display: inline-block;
|
||||
|
||||
@@ -1118,6 +1121,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// view - student admin
|
||||
// --------------------
|
||||
.instructor-dashboard-wrapper-2 section.idash-section#student_admin > {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<%- gettext('Students in this cohort are:') %>
|
||||
</h4>
|
||||
<label>
|
||||
<input type="radio" class="type-random" name="cohort-assignment-type" value="random" <%- assignment_type == 'random' ? 'checked="checked"' : '' %>/> <%- gettext("Randomly Assigned") %>
|
||||
<input type="radio" class="type-random" name="cohort-assignment-type" value="random" <%- assignment_type == 'random' ? 'checked="checked"' : '' %>/> <%- gettext("Automatically Assigned") %>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" class="type-manual" name="cohort-assignment-type" value="manual" <%- assignment_type == 'manual' || isNewCohort ? 'checked="checked"' : '' %>/> <%- gettext("Manually Assigned") %>
|
||||
@@ -47,7 +47,7 @@
|
||||
<% if (isDefaultCohort) { %>
|
||||
<p class="copy-error">
|
||||
<i class="icon fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<%- gettext("There must be one cohort to which students can be randomly assigned.") %>
|
||||
<%- gettext("There must be one cohort to which students can automatically be assigned.") %>
|
||||
</p>
|
||||
<% } %>
|
||||
<hr class="divider divider-lv1">
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="nav-utilities">
|
||||
<span class="action-toggle-message" aria-live="polite"></span>
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%page args="section_data"/>
|
||||
<%! from courseware.courses import get_studio_url %>
|
||||
<%! from microsite_configuration import microsite %>
|
||||
<%! from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition %>
|
||||
|
||||
|
||||
<div class="cohort-management"
|
||||
data-cohorts_url="${section_data['cohorts_url']}"
|
||||
data-advanced-settings-url="${section_data['advanced_settings_url']}"
|
||||
data-upload_cohorts_csv_url="${section_data['upload_cohorts_csv_url']}"
|
||||
data-course_cohort_settings_url="${section_data['course_cohort_settings_url']}"
|
||||
>
|
||||
</div>
|
||||
|
||||
|
||||
<%block name="headextra">
|
||||
<%
|
||||
cohorted_user_partition = get_cohorted_user_partition(course.id)
|
||||
content_groups = cohorted_user_partition.groups if cohorted_user_partition else []
|
||||
%>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
|
||||
var cohortUserPartitionId = ${cohorted_user_partition.id if cohorted_user_partition else 'null'},
|
||||
contentGroups = [
|
||||
% for content_group in content_groups:
|
||||
new edx.groups.ContentGroupModel({
|
||||
id: ${content_group.id},
|
||||
name: "${content_group.name | h}",
|
||||
user_partition_id: cohortUserPartitionId
|
||||
}),
|
||||
% endfor
|
||||
];
|
||||
|
||||
(function (require) {
|
||||
require(['js/factories/cohorts_factory'], function (CohortsFactory) {
|
||||
CohortsFactory(contentGroups, '${get_studio_url(course, 'group_configurations') | h}');
|
||||
});
|
||||
}).call(this, require || RequireJS.require);
|
||||
});
|
||||
</script>
|
||||
|
||||
</%block>
|
||||
<div class="cohort-state-message"></div>
|
||||
@@ -1,50 +1,51 @@
|
||||
<h2 class="section-title">
|
||||
<span class="value"><%- gettext('Cohort Management') %></span>
|
||||
<span class="description"></span>
|
||||
</h2>
|
||||
|
||||
<div class="cohort-management-nav">
|
||||
<h3 class="subsection-title"><%- gettext('Assign students to cohorts manually') %></h3>
|
||||
<form action="" method="post" name="" id="cohort-management-nav-form" class="cohort-management-nav-form">
|
||||
|
||||
<div class="cohort-management-nav-form-select field field-select">
|
||||
<label for="cohort-select" class="label sr"><%- gettext("Select a cohort group to manage") %></label>
|
||||
<select class="input cohort-select" name="cohort-select" id="cohort-select"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="form-submit button action-primary action-view sr"><%- gettext('View Cohort') %></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<a href="" class="action-primary action-create">
|
||||
<i class="icon fa fa-plus" aria-hidden="true"></i>
|
||||
<%- gettext('Add Cohort') %>
|
||||
</a>
|
||||
<div class="cohorts-state-section">
|
||||
<label> <input type="checkbox" class="cohorts-state" value="Cohorts-State" <%- cohortsEnabled ? 'checked="checked"' : '' %> /> <%- gettext('Enable Cohorts') %></label>
|
||||
</div>
|
||||
|
||||
<!-- Add modal -->
|
||||
<div class="cohort-management-add-form"></div>
|
||||
<% if (cohortsEnabled) { %>
|
||||
<div class="cohort-management-nav">
|
||||
<hr class="divider divider-lv1" />
|
||||
<form action="" method="post" name="" id="cohort-management-nav-form" class="cohort-management-nav-form">
|
||||
|
||||
<!-- individual group -->
|
||||
<div class="cohort-management-group"></div>
|
||||
<div class="cohort-management-nav-form-select field field-select">
|
||||
<label for="cohort-select" class="label sr"><%- gettext("Select a cohort group to manage") %></label>
|
||||
<select class="input cohort-select" name="cohort-select" id="cohort-select"></select>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-cohort-supplemental">
|
||||
<div class="form-actions">
|
||||
<button class="form-submit button action-primary action-view sr"><%- gettext('View Cohort') %></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="divider divider-lv1" />
|
||||
|
||||
<!-- Uploading a CSV file of cohort assignments. -->
|
||||
<a class="toggle-cohort-management-secondary" href="#cohort-management-file-upload"><%- gettext('Assign students to cohorts by uploading a CSV file') %></a>
|
||||
<div class="cohort-management-file-upload csv-upload is-hidden" id="cohort-management-file-upload"></div>
|
||||
|
||||
<div class="cohort-management-supplemental">
|
||||
<p class="">
|
||||
<i class="icon fa fa-info-circle" aria-hidden="true"></i>
|
||||
<%= interpolate(
|
||||
gettext('To review student cohort assignments or see the results of uploading a CSV file, download course profile information or cohort results on %(link_start)s the Data Download page. %(link_end)s'),
|
||||
{link_start: '<a href="" class="link-cross-reference" data-section="data_download">', link_end: '</a>'},
|
||||
true
|
||||
) %>
|
||||
</p>
|
||||
<a href="" class="action-primary action-create">
|
||||
<i class="icon fa fa-plus" aria-hidden="true"></i>
|
||||
<%- gettext('Add Cohort') %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add modal -->
|
||||
<div class="cohort-management-add-form"></div>
|
||||
|
||||
<!-- individual group -->
|
||||
<div class="cohort-management-group"></div>
|
||||
|
||||
<div class="wrapper-cohort-supplemental">
|
||||
|
||||
<hr class="divider divider-lv1" />
|
||||
|
||||
<!-- Uploading a CSV file of cohort assignments. -->
|
||||
<a class="toggle-cohort-management-secondary" href="#cohort-management-file-upload"><%- gettext('Assign students to cohorts by uploading a CSV file') %></a>
|
||||
<div class="cohort-management-file-upload csv-upload is-hidden" id="cohort-management-file-upload"></div>
|
||||
|
||||
<div class="cohort-management-supplemental">
|
||||
<p class="">
|
||||
<i class="icon fa fa-info-circle" aria-hidden="true"></i>
|
||||
<%= interpolate(
|
||||
gettext('To review student cohort assignments or see the results of uploading a CSV file, download course profile information or cohort results on %(link_start)s the Data Download page. %(link_end)s'),
|
||||
{link_start: '<a href="" class="link-cross-reference" data-section="data_download">', link_end: '</a>'},
|
||||
true
|
||||
) %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
@@ -53,12 +53,16 @@
|
||||
<%static:js group='application'/>
|
||||
|
||||
## Backbone classes declared explicitly until RequireJS is supported
|
||||
<script type="text/javascript" src="${static.url('js/instructor_dashboard/ecommerce.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/instructor_dashboard/cohort_management.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/notification.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/notification.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/file_uploader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/groups/models/cohort.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/groups/models/content_group.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/groups/models/course_cohort_settings.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/groups/collections/cohort.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/groups/views/course_cohort_settings_notification.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/groups/views/cohort_form.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/groups/views/cohort_editor.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/groups/views/cohorts.js')}"></script>
|
||||
@@ -66,7 +70,7 @@
|
||||
|
||||
## Include Underscore templates
|
||||
<%block name="header_extras">
|
||||
% for template_name in ["cohorts", "cohort-editor", "cohort-group-header", "cohort-selector", "cohort-form", "notification"]:
|
||||
% for template_name in ["cohorts", "cohort-editor", "cohort-group-header", "cohort-selector", "cohort-form", "notification", "cohort-state"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="instructor/instructor_dashboard_2/${template_name}.underscore" />
|
||||
</script>
|
||||
@@ -125,4 +129,4 @@
|
||||
% endfor
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
@@ -245,53 +245,3 @@
|
||||
%endif
|
||||
|
||||
</div>
|
||||
|
||||
% if course.is_cohorted:
|
||||
<hr class="divider" />
|
||||
<div class="cohort-management membership-section"
|
||||
data-ajax_url="${section_data['cohorts_ajax_url']}"
|
||||
data-advanced-settings-url="${section_data['advanced_settings_url']}"
|
||||
data-upload_cohorts_csv_url="${section_data['upload_cohorts_csv_url']}"
|
||||
>
|
||||
</div>
|
||||
|
||||
<%block name="headextra">
|
||||
<%
|
||||
cohorted_user_partition = get_cohorted_user_partition(course.id)
|
||||
content_groups = cohorted_user_partition.groups if cohorted_user_partition else []
|
||||
%>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var cohortManagementElement = $('.cohort-management');
|
||||
if (cohortManagementElement.length > 0) {
|
||||
var cohorts = new edx.groups.CohortCollection(),
|
||||
cohortUserPartitionId = ${cohorted_user_partition.id if cohorted_user_partition else 'null'},
|
||||
contentGroups = [
|
||||
% for content_group in content_groups:
|
||||
new edx.groups.ContentGroupModel({
|
||||
id: ${content_group.id},
|
||||
name: "${content_group.name | h}",
|
||||
user_partition_id: cohortUserPartitionId
|
||||
}),
|
||||
% endfor
|
||||
];
|
||||
cohorts.url = cohortManagementElement.data('ajax_url');
|
||||
var cohortsView = new edx.groups.CohortsView({
|
||||
el: cohortManagementElement,
|
||||
model: cohorts,
|
||||
contentGroups: contentGroups,
|
||||
context: {
|
||||
uploadCohortsCsvUrl: cohortManagementElement.data('upload_cohorts_csv_url'),
|
||||
studioAdvancedSettingsUrl: cohortManagementElement.data('advanced-settings-url'),
|
||||
studioGroupConfigurationsUrl: '${get_studio_url(course, 'group_configurations') | h}'
|
||||
}
|
||||
});
|
||||
cohorts.fetch().done(function() {
|
||||
cohortsView.render();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</%block>
|
||||
% endif
|
||||
|
||||
@@ -379,6 +379,9 @@ if settings.COURSEWARE_ENABLED:
|
||||
'open_ended_grading.views.take_action_on_flags', name='open_ended_flagged_problems_take_action'),
|
||||
|
||||
# Cohorts management
|
||||
url(r'^courses/{}/cohorts/settings$'.format(settings.COURSE_KEY_PATTERN),
|
||||
'openedx.core.djangoapps.course_groups.views.course_cohort_settings_handler',
|
||||
name="course_cohort_settings"),
|
||||
url(r'^courses/{}/cohorts/(?P<cohort_id>[0-9]+)?$'.format(settings.COURSE_KEY_PATTERN),
|
||||
'openedx.core.djangoapps.course_groups.views.cohort_handler', name="cohorts"),
|
||||
url(r'^courses/{}/cohorts/(?P<cohort_id>[0-9]+)$'.format(settings.COURSE_KEY_PATTERN),
|
||||
|
||||
@@ -5,7 +5,6 @@ forums, and to the cohort admin views.
|
||||
|
||||
import logging
|
||||
import random
|
||||
import json
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models.signals import post_save, m2m_changed
|
||||
@@ -238,7 +237,7 @@ def migrate_cohort_settings(course):
|
||||
course_id=course_id,
|
||||
defaults={
|
||||
'is_cohorted': course.is_cohorted,
|
||||
'cohorted_discussions': json.dumps(list(course.cohorted_discussions)),
|
||||
'cohorted_discussions': list(course.cohorted_discussions),
|
||||
'always_cohort_inline_discussions': course.always_cohort_inline_discussions
|
||||
}
|
||||
)
|
||||
@@ -324,7 +323,8 @@ def add_cohort(course_key, name, assignment_type):
|
||||
raise ValueError("Invalid course_key")
|
||||
|
||||
cohort = CourseCohort.create(
|
||||
cohort_name=name, course_id=course.id,
|
||||
cohort_name=name,
|
||||
course_id=course.id,
|
||||
assignment_type=assignment_type
|
||||
).course_user_group
|
||||
|
||||
@@ -413,7 +413,7 @@ def set_assignment_type(user_group, assignment_type):
|
||||
course_cohort = user_group.cohort
|
||||
|
||||
if is_default_cohort(user_group) and course_cohort.assignment_type != assignment_type:
|
||||
raise ValueError(_("There must be one cohort to which students can be randomly assigned."))
|
||||
raise ValueError(_("There must be one cohort to which students can automatically be assigned."))
|
||||
|
||||
course_cohort.assignment_type = assignment_type
|
||||
course_cohort.save()
|
||||
@@ -438,3 +438,48 @@ def is_default_cohort(user_group):
|
||||
)
|
||||
|
||||
return len(random_cohorts) == 1 and random_cohorts[0].name == user_group.name
|
||||
|
||||
|
||||
def set_course_cohort_settings(course_key, **kwargs):
|
||||
"""
|
||||
Set cohort settings for a course.
|
||||
|
||||
Arguments:
|
||||
course_key: CourseKey
|
||||
is_cohorted (bool): If the course should be cohorted.
|
||||
always_cohort_inline_discussions (bool): If inline discussions should always be cohorted.
|
||||
cohorted_discussions (list): List of discussion ids.
|
||||
|
||||
Returns:
|
||||
A CourseCohortSettings object.
|
||||
|
||||
Raises:
|
||||
ValueError if course_key is invalid.
|
||||
"""
|
||||
course_cohort_settings = get_course_cohort_settings(course_key)
|
||||
for field in ('is_cohorted', 'always_cohort_inline_discussions', 'cohorted_discussions'):
|
||||
if field in kwargs:
|
||||
setattr(course_cohort_settings, field, kwargs[field])
|
||||
course_cohort_settings.save()
|
||||
return course_cohort_settings
|
||||
|
||||
|
||||
def get_course_cohort_settings(course_key):
|
||||
"""
|
||||
Return cohort settings for a course.
|
||||
|
||||
Arguments:
|
||||
course_key: CourseKey
|
||||
|
||||
Returns:
|
||||
A CourseCohortSettings object.
|
||||
|
||||
Raises:
|
||||
ValueError if course_key is invalid.
|
||||
"""
|
||||
try:
|
||||
course_cohort_settings = CourseCohortsSettings.objects.get(course_id=course_key)
|
||||
except CourseCohortsSettings.DoesNotExist:
|
||||
course = courses.get_course(course_key)
|
||||
course_cohort_settings = migrate_cohort_settings(course)
|
||||
return course_cohort_settings
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Changed 'CourseCohortsSettings.cohorted_discussions' to 'CourseCohortsSettings._cohorted_discussions' without
|
||||
# changing db column name
|
||||
pass
|
||||
|
||||
def backwards(self, orm):
|
||||
# Changed 'CourseCohortsSettings.cohorted_discussions' to 'CourseCohortsSettings._cohorted_discussions' without
|
||||
# changing db column name
|
||||
pass
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'course_groups.coursecohort': {
|
||||
'Meta': {'object_name': 'CourseCohort'},
|
||||
'assignment_type': ('django.db.models.fields.CharField', [], {'default': "'manual'", 'max_length': '20'}),
|
||||
'course_user_group': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'cohort'", 'unique': 'True', 'to': "orm['course_groups.CourseUserGroup']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
'course_groups.coursecohortssettings': {
|
||||
'Meta': {'object_name': 'CourseCohortsSettings'},
|
||||
'_cohorted_discussions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'cohorted_discussions'", 'blank': 'True'}),
|
||||
'always_cohort_inline_discussions': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_cohorted': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
|
||||
},
|
||||
'course_groups.courseusergroup': {
|
||||
'Meta': {'unique_together': "(('name', 'course_id'),)", 'object_name': 'CourseUserGroup'},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'group_type': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'users': ('django.db.models.fields.related.ManyToManyField', [], {'db_index': 'True', 'related_name': "'course_groups'", 'symmetrical': 'False', 'to': "orm['auth.User']"})
|
||||
},
|
||||
'course_groups.courseusergrouppartitiongroup': {
|
||||
'Meta': {'object_name': 'CourseUserGroupPartitionGroup'},
|
||||
'course_user_group': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['course_groups.CourseUserGroup']", 'unique': 'True'}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'group_id': ('django.db.models.fields.IntegerField', [], {}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'partition_id': ('django.db.models.fields.IntegerField', [], {}),
|
||||
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['course_groups']
|
||||
@@ -1,3 +1,8 @@
|
||||
"""
|
||||
Django models related to course groups functionality.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
@@ -81,11 +86,21 @@ class CourseCohortsSettings(models.Model):
|
||||
help_text="Which course are these settings associated with?",
|
||||
)
|
||||
|
||||
cohorted_discussions = models.TextField(null=True, blank=True) # JSON list
|
||||
_cohorted_discussions = models.TextField(db_column='cohorted_discussions', null=True, blank=True) # JSON list
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
always_cohort_inline_discussions = models.BooleanField(default=True)
|
||||
|
||||
@property
|
||||
def cohorted_discussions(self):
|
||||
"""Jsonfiy the cohorted_discussions"""
|
||||
return json.loads(self._cohorted_discussions)
|
||||
|
||||
@cohorted_discussions.setter
|
||||
def cohorted_discussions(self, value):
|
||||
"""UnJsonfiy the cohorted_discussions"""
|
||||
self._cohorted_discussions = json.dumps(value)
|
||||
|
||||
|
||||
class CourseCohort(models.Model):
|
||||
"""
|
||||
|
||||
@@ -8,7 +8,9 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
|
||||
from ..models import CourseUserGroup, CourseCohort
|
||||
from ..models import CourseUserGroup, CourseCohort, CourseCohortsSettings
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class CohortFactory(DjangoModelFactory):
|
||||
@@ -40,6 +42,19 @@ class CourseCohortFactory(DjangoModelFactory):
|
||||
assignment_type = 'manual'
|
||||
|
||||
|
||||
class CourseCohortSettingsFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for constructing mock course cohort settings.
|
||||
"""
|
||||
FACTORY_FOR = CourseCohortsSettings
|
||||
|
||||
is_cohorted = False
|
||||
course_id = SlashSeparatedCourseKey("dummy", "dummy", "dummy")
|
||||
cohorted_discussions = json.dumps([])
|
||||
# pylint: disable=invalid-name
|
||||
always_cohort_inline_discussions = True
|
||||
|
||||
|
||||
def topic_name_to_id(course, name):
|
||||
"""
|
||||
Given a discussion topic name, return an id for that name (includes
|
||||
|
||||
@@ -3,7 +3,6 @@ Tests for cohorts
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import IntegrityError
|
||||
from django.http import Http404
|
||||
@@ -19,8 +18,9 @@ from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTO
|
||||
|
||||
from ..models import CourseUserGroup, CourseCohort, CourseUserGroupPartitionGroup
|
||||
from .. import cohorts
|
||||
from ..tests.helpers import topic_name_to_id, config_course_cohorts, CohortFactory, CourseCohortFactory
|
||||
|
||||
from ..tests.helpers import (
|
||||
topic_name_to_id, config_course_cohorts, CohortFactory, CourseCohortFactory, CourseCohortSettingsFactory
|
||||
)
|
||||
|
||||
@patch("openedx.core.djangoapps.course_groups.cohorts.tracker")
|
||||
class TestCohortSignals(TestCase):
|
||||
@@ -209,7 +209,7 @@ class TestCohorts(ModuleStoreTestCase):
|
||||
|
||||
self.assertEqual(cohorts.get_assignment_type(cohort), CourseCohort.RANDOM)
|
||||
|
||||
exception_msg = "There must be one cohort to which students can be randomly assigned."
|
||||
exception_msg = "There must be one cohort to which students can automatically be assigned."
|
||||
with self.assertRaises(ValueError) as context_manager:
|
||||
cohorts.set_assignment_type(cohort, CourseCohort.MANUAL)
|
||||
|
||||
@@ -685,12 +685,43 @@ class TestCohorts(ModuleStoreTestCase):
|
||||
lambda: cohorts.add_user_to_cohort(first_cohort, "non_existent_username")
|
||||
)
|
||||
|
||||
def test_get_course_cohort_settings(self):
|
||||
"""
|
||||
Test that cohorts.get_course_cohort_settings is working as expected.
|
||||
"""
|
||||
course = modulestore().get_course(self.toy_course_key)
|
||||
course_cohort_settings = cohorts.get_course_cohort_settings(course.id)
|
||||
|
||||
self.assertFalse(course_cohort_settings.is_cohorted)
|
||||
self.assertEqual(course_cohort_settings.cohorted_discussions, [])
|
||||
self.assertTrue(course_cohort_settings.always_cohort_inline_discussions)
|
||||
|
||||
def test_update_course_cohort_settings(self):
|
||||
"""
|
||||
Test that cohorts.set_course_cohort_settings is working as expected.
|
||||
"""
|
||||
course = modulestore().get_course(self.toy_course_key)
|
||||
CourseCohortSettingsFactory(course_id=course.id)
|
||||
|
||||
cohorts.set_course_cohort_settings(
|
||||
course.id,
|
||||
is_cohorted=False,
|
||||
cohorted_discussions=['topic a id', 'topic b id'],
|
||||
always_cohort_inline_discussions=False
|
||||
)
|
||||
|
||||
course_cohort_settings = cohorts.get_course_cohort_settings(course.id)
|
||||
|
||||
self.assertFalse(course_cohort_settings.is_cohorted)
|
||||
self.assertEqual(course_cohort_settings.cohorted_discussions, ['topic a id', 'topic b id'])
|
||||
self.assertFalse(course_cohort_settings.always_cohort_inline_discussions)
|
||||
|
||||
|
||||
class TestCohortsAndPartitionGroups(ModuleStoreTestCase):
|
||||
MODULESTORE = TEST_DATA_MIXED_TOY_MODULESTORE
|
||||
"""
|
||||
Test Cohorts and Partitions Groups.
|
||||
"""
|
||||
MODULESTORE = TEST_DATA_MIXED_TOY_MODULESTORE
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
|
||||
@@ -20,13 +20,14 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from ..models import CourseUserGroup, CourseCohort
|
||||
from ..views import (
|
||||
cohort_handler, users_in_cohort, add_users_to_cohort, remove_user_from_cohort, link_cohort_to_partition_group
|
||||
course_cohort_settings_handler, cohort_handler, users_in_cohort, add_users_to_cohort, remove_user_from_cohort,
|
||||
link_cohort_to_partition_group
|
||||
)
|
||||
from ..cohorts import (
|
||||
get_cohort, get_cohort_by_name, get_cohort_by_id,
|
||||
DEFAULT_COHORT_NAME, get_group_info_for_cohort
|
||||
)
|
||||
from .helpers import config_course_cohorts, CohortFactory, CourseCohortFactory
|
||||
from .helpers import config_course_cohorts, CohortFactory, CourseCohortFactory, topic_name_to_id
|
||||
|
||||
|
||||
class CohortViewsTestCase(ModuleStoreTestCase):
|
||||
@@ -90,49 +91,114 @@ class CohortViewsTestCase(ModuleStoreTestCase):
|
||||
view_args.insert(0, request)
|
||||
self.assertRaises(Http404, view, *view_args)
|
||||
|
||||
|
||||
class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
"""
|
||||
Tests the `cohort_handler` view.
|
||||
"""
|
||||
def get_cohort_handler(self, course, cohort=None):
|
||||
def get_handler(self, course, cohort=None, expected_response_code=200, handler=cohort_handler):
|
||||
"""
|
||||
Call a GET on `cohort_handler` for a given `course` and return its response as a
|
||||
dict. If `cohort` is specified, only information for that specific cohort is returned.
|
||||
Call a GET on `handler` for a given `course` and return its response as a dict.
|
||||
Raise an exception if response status code is not as expected.
|
||||
"""
|
||||
request = RequestFactory().get("dummy_url")
|
||||
request.user = self.staff_user
|
||||
if cohort:
|
||||
response = cohort_handler(request, unicode(course.id), cohort.id)
|
||||
response = handler(request, unicode(course.id), cohort.id)
|
||||
else:
|
||||
response = cohort_handler(request, unicode(course.id))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = handler(request, unicode(course.id))
|
||||
self.assertEqual(response.status_code, expected_response_code)
|
||||
return json.loads(response.content)
|
||||
|
||||
def put_cohort_handler(self, course, cohort=None, data=None, expected_response_code=200):
|
||||
def put_handler(self, course, cohort=None, data=None, expected_response_code=200, handler=cohort_handler):
|
||||
"""
|
||||
Call a PUT on `cohort_handler` for a given `course` and return its response as a
|
||||
dict. If `cohort` is not specified, a new cohort is created. If `cohort` is specified,
|
||||
the existing cohort is updated.
|
||||
Call a PUT on `handler` for a given `course` and return its response as a dict.
|
||||
Raise an exception if response status code is not as expected.
|
||||
"""
|
||||
if not isinstance(data, basestring):
|
||||
data = json.dumps(data or {})
|
||||
request = RequestFactory().put(path="dummy path", data=data, content_type="application/json")
|
||||
request.user = self.staff_user
|
||||
if cohort:
|
||||
response = cohort_handler(request, unicode(course.id), cohort.id)
|
||||
response = handler(request, unicode(course.id), cohort.id)
|
||||
else:
|
||||
response = cohort_handler(request, unicode(course.id))
|
||||
response = handler(request, unicode(course.id))
|
||||
self.assertEqual(response.status_code, expected_response_code)
|
||||
return json.loads(response.content)
|
||||
|
||||
|
||||
class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase):
|
||||
"""
|
||||
Tests the `course_cohort_settings_handler` view.
|
||||
"""
|
||||
def test_non_staff(self):
|
||||
"""
|
||||
Verify that we cannot access course_cohort_settings_handler if we're a non-staff user.
|
||||
"""
|
||||
self._verify_non_staff_cannot_access(course_cohort_settings_handler, "GET", [unicode(self.course.id)])
|
||||
self._verify_non_staff_cannot_access(course_cohort_settings_handler, "PUT", [unicode(self.course.id)])
|
||||
|
||||
def test_get_settings(self):
|
||||
"""
|
||||
Verify that course_cohort_settings_handler is working for HTTP GET.
|
||||
"""
|
||||
cohorted_discussions = ['Topic A', 'Topic B']
|
||||
config_course_cohorts(self.course, [], cohorted=True, cohorted_discussions=cohorted_discussions)
|
||||
|
||||
response = self.get_handler(self.course, handler=course_cohort_settings_handler)
|
||||
response['cohorted_discussions'].sort()
|
||||
|
||||
expected_response = {
|
||||
'is_cohorted': True,
|
||||
'always_cohort_inline_discussions': True,
|
||||
'cohorted_discussions': [topic_name_to_id(self.course, name) for name in cohorted_discussions],
|
||||
'id': 1
|
||||
}
|
||||
expected_response['cohorted_discussions'].sort()
|
||||
|
||||
self.assertEqual(response, expected_response)
|
||||
|
||||
def test_update_settings(self):
|
||||
"""
|
||||
Verify that course_cohort_settings_handler is working for HTTP POST.
|
||||
"""
|
||||
config_course_cohorts(self.course, [], cohorted=True)
|
||||
|
||||
response = self.get_handler(self.course, handler=course_cohort_settings_handler)
|
||||
|
||||
expected_response = {
|
||||
'is_cohorted': True,
|
||||
'always_cohort_inline_discussions': True,
|
||||
'cohorted_discussions': [],
|
||||
'id': 1
|
||||
}
|
||||
self.assertEqual(response, expected_response)
|
||||
|
||||
expected_response['is_cohorted'] = False
|
||||
response = self.put_handler(self.course, data=expected_response, handler=course_cohort_settings_handler)
|
||||
|
||||
self.assertEqual(response, expected_response)
|
||||
|
||||
def test_update_settings_with_missing_field(self):
|
||||
"""
|
||||
Verify that course_cohort_settings_handler return HTTP 400 if required data field is missing from post data.
|
||||
"""
|
||||
config_course_cohorts(self.course, [], cohorted=True)
|
||||
|
||||
# Get the cohorts from the course. This will run the migrations.
|
||||
# And due to migrations CourseCohortsSettings object will be created.
|
||||
self.get_handler(self.course)
|
||||
|
||||
response = self.put_handler(self.course, expected_response_code=400, handler=course_cohort_settings_handler)
|
||||
self.assertEqual("Bad Request", response.get("error"))
|
||||
|
||||
|
||||
class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
"""
|
||||
Tests the `cohort_handler` view.
|
||||
"""
|
||||
def verify_lists_expected_cohorts(self, expected_cohorts, response_dict=None):
|
||||
"""
|
||||
Verify that the server response contains the expected_cohorts.
|
||||
If response_dict is None, the list of cohorts is requested from the server.
|
||||
"""
|
||||
if response_dict is None:
|
||||
response_dict = self.get_cohort_handler(self.course)
|
||||
response_dict = self.get_handler(self.course)
|
||||
|
||||
self.assertEqual(
|
||||
response_dict.get("cohorts"),
|
||||
@@ -197,7 +263,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
# Will create cohort1, cohort2, and cohort3. Auto cohorts remain uncreated.
|
||||
self._create_cohorts()
|
||||
# Get the cohorts from the course, which will cause auto cohorts to be created.
|
||||
actual_cohorts = self.get_cohort_handler(self.course)
|
||||
actual_cohorts = self.get_handler(self.course)
|
||||
# Get references to the created auto cohorts.
|
||||
auto_cohort_1 = get_cohort_by_name(self.course.id, "AutoGroup1")
|
||||
auto_cohort_2 = get_cohort_by_name(self.course.id, "AutoGroup2")
|
||||
@@ -235,7 +301,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
|
||||
# verify the default cohort is automatically created
|
||||
default_cohort = get_cohort_by_name(self.course.id, DEFAULT_COHORT_NAME)
|
||||
actual_cohorts = self.get_cohort_handler(self.course)
|
||||
actual_cohorts = self.get_handler(self.course)
|
||||
self.verify_lists_expected_cohorts(
|
||||
[CohortHandlerTestCase.create_expected_cohort(default_cohort, len(users), CourseCohort.RANDOM)],
|
||||
actual_cohorts,
|
||||
@@ -255,7 +321,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
Tests that information for just a single cohort can be requested.
|
||||
"""
|
||||
self._create_cohorts()
|
||||
response_dict = self.get_cohort_handler(self.course, self.cohort2)
|
||||
response_dict = self.get_handler(self.course, self.cohort2)
|
||||
self.assertEqual(
|
||||
response_dict,
|
||||
{
|
||||
@@ -298,7 +364,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
"""
|
||||
new_cohort_name = "New cohort unassociated to content groups"
|
||||
request_data = {'name': new_cohort_name, 'assignment_type': CourseCohort.RANDOM}
|
||||
response_dict = self.put_cohort_handler(self.course, data=request_data)
|
||||
response_dict = self.put_handler(self.course, data=request_data)
|
||||
self.verify_contains_added_cohort(response_dict, new_cohort_name, assignment_type=CourseCohort.RANDOM)
|
||||
|
||||
new_cohort_name = "New cohort linked to group"
|
||||
@@ -308,7 +374,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
'user_partition_id': 1,
|
||||
'group_id': 2
|
||||
}
|
||||
response_dict = self.put_cohort_handler(self.course, data=data)
|
||||
response_dict = self.put_handler(self.course, data=data)
|
||||
self.verify_contains_added_cohort(
|
||||
response_dict,
|
||||
new_cohort_name,
|
||||
@@ -320,14 +386,14 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
"""
|
||||
Verify that we cannot create a cohort without specifying a name.
|
||||
"""
|
||||
response_dict = self.put_cohort_handler(self.course, expected_response_code=400)
|
||||
response_dict = self.put_handler(self.course, expected_response_code=400)
|
||||
self.assertEqual("Cohort name must be specified.", response_dict.get("error"))
|
||||
|
||||
def test_create_new_cohort_missing_assignment_type(self):
|
||||
"""
|
||||
Verify that we cannot create a cohort without specifying an assignment type.
|
||||
"""
|
||||
response_dict = self.put_cohort_handler(self.course, data={'name': 'COHORT NAME'}, expected_response_code=400)
|
||||
response_dict = self.put_handler(self.course, data={'name': 'COHORT NAME'}, expected_response_code=400)
|
||||
self.assertEqual("Assignment type must be specified.", response_dict.get("error"))
|
||||
|
||||
def test_create_new_cohort_existing_name(self):
|
||||
@@ -335,7 +401,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
Verify that we cannot add a cohort with the same name as an existing cohort.
|
||||
"""
|
||||
self._create_cohorts()
|
||||
response_dict = self.put_cohort_handler(
|
||||
response_dict = self.put_handler(
|
||||
self.course, data={'name': self.cohort1.name, 'assignment_type': CourseCohort.MANUAL},
|
||||
expected_response_code=400
|
||||
)
|
||||
@@ -346,7 +412,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
Verify that we cannot create a cohort with a group_id if the user_partition_id is not also specified.
|
||||
"""
|
||||
data = {'name': "Cohort missing user_partition_id", 'assignment_type': CourseCohort.MANUAL, 'group_id': 2}
|
||||
response_dict = self.put_cohort_handler(self.course, data=data, expected_response_code=400)
|
||||
response_dict = self.put_handler(self.course, data=data, expected_response_code=400)
|
||||
self.assertEqual(
|
||||
"If group_id is specified, user_partition_id must also be specified.", response_dict.get("error")
|
||||
)
|
||||
@@ -360,7 +426,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
self._create_cohorts()
|
||||
updated_name = self.cohort1.name + "_updated"
|
||||
data = {'name': updated_name, 'assignment_type': CourseCohort.MANUAL}
|
||||
response_dict = self.put_cohort_handler(self.course, self.cohort1, data=data)
|
||||
response_dict = self.put_handler(self.course, self.cohort1, data=data)
|
||||
self.assertEqual(updated_name, get_cohort_by_id(self.course.id, self.cohort1.id).name)
|
||||
self.assertEqual(updated_name, response_dict.get("name"))
|
||||
self.assertEqual(CourseCohort.MANUAL, response_dict.get("assignment_type"))
|
||||
@@ -372,7 +438,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
# Create a new cohort with random assignment
|
||||
cohort_name = 'I AM A RANDOM COHORT'
|
||||
data = {'name': cohort_name, 'assignment_type': CourseCohort.RANDOM}
|
||||
response_dict = self.put_cohort_handler(self.course, data=data)
|
||||
response_dict = self.put_handler(self.course, data=data)
|
||||
|
||||
self.assertEqual(cohort_name, response_dict.get("name"))
|
||||
self.assertEqual(CourseCohort.RANDOM, response_dict.get("assignment_type"))
|
||||
@@ -381,7 +447,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
newly_created_cohort = get_cohort_by_name(self.course.id, cohort_name)
|
||||
cohort_name = 'I AM AN UPDATED RANDOM COHORT'
|
||||
data = {'name': cohort_name, 'assignment_type': CourseCohort.RANDOM}
|
||||
response_dict = self.put_cohort_handler(self.course, newly_created_cohort, data=data)
|
||||
response_dict = self.put_handler(self.course, newly_created_cohort, data=data)
|
||||
|
||||
self.assertEqual(cohort_name, get_cohort_by_id(self.course.id, newly_created_cohort.id).name)
|
||||
self.assertEqual(cohort_name, response_dict.get("name"))
|
||||
@@ -394,7 +460,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
# Create a new cohort with random assignment
|
||||
cohort_name = 'I AM A RANDOM COHORT'
|
||||
data = {'name': cohort_name, 'assignment_type': CourseCohort.RANDOM}
|
||||
response_dict = self.put_cohort_handler(self.course, data=data)
|
||||
response_dict = self.put_handler(self.course, data=data)
|
||||
|
||||
self.assertEqual(cohort_name, response_dict.get("name"))
|
||||
self.assertEqual(CourseCohort.RANDOM, response_dict.get("assignment_type"))
|
||||
@@ -402,9 +468,9 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
# Try to update the assignment type of newly created random cohort
|
||||
cohort = get_cohort_by_name(self.course.id, cohort_name)
|
||||
data = {'name': cohort_name, 'assignment_type': CourseCohort.MANUAL}
|
||||
response_dict = self.put_cohort_handler(self.course, cohort, data=data, expected_response_code=400)
|
||||
response_dict = self.put_handler(self.course, cohort, data=data, expected_response_code=400)
|
||||
self.assertEqual(
|
||||
'There must be one cohort to which students can be randomly assigned.', response_dict.get("error")
|
||||
'There must be one cohort to which students can automatically be assigned.', response_dict.get("error")
|
||||
)
|
||||
|
||||
def test_update_cohort_group_id(self):
|
||||
@@ -419,7 +485,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
'group_id': 2,
|
||||
'user_partition_id': 3
|
||||
}
|
||||
response_dict = self.put_cohort_handler(self.course, self.cohort1, data=data)
|
||||
response_dict = self.put_handler(self.course, self.cohort1, data=data)
|
||||
self.assertEqual((2, 3), get_group_info_for_cohort(self.cohort1))
|
||||
self.assertEqual(2, response_dict.get("group_id"))
|
||||
self.assertEqual(3, response_dict.get("user_partition_id"))
|
||||
@@ -434,7 +500,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
link_cohort_to_partition_group(self.cohort1, 5, 0)
|
||||
self.assertEqual((0, 5), get_group_info_for_cohort(self.cohort1))
|
||||
data = {'name': self.cohort1.name, 'assignment_type': CourseCohort.RANDOM, 'group_id': None}
|
||||
response_dict = self.put_cohort_handler(self.course, self.cohort1, data=data)
|
||||
response_dict = self.put_handler(self.course, self.cohort1, data=data)
|
||||
self.assertEqual((None, None), get_group_info_for_cohort(self.cohort1))
|
||||
self.assertIsNone(response_dict.get("group_id"))
|
||||
self.assertIsNone(response_dict.get("user_partition_id"))
|
||||
@@ -452,7 +518,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
'group_id': 2,
|
||||
'user_partition_id': 3
|
||||
}
|
||||
self.put_cohort_handler(self.course, self.cohort4, data=data)
|
||||
self.put_handler(self.course, self.cohort4, data=data)
|
||||
self.assertEqual((2, 3), get_group_info_for_cohort(self.cohort4))
|
||||
|
||||
data = {
|
||||
@@ -461,7 +527,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
'group_id': 1,
|
||||
'user_partition_id': 3
|
||||
}
|
||||
self.put_cohort_handler(self.course, self.cohort4, data=data)
|
||||
self.put_handler(self.course, self.cohort4, data=data)
|
||||
self.assertEqual((1, 3), get_group_info_for_cohort(self.cohort4))
|
||||
|
||||
def test_update_cohort_missing_user_partition_id(self):
|
||||
@@ -470,7 +536,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
|
||||
"""
|
||||
self._create_cohorts()
|
||||
data = {'name': self.cohort1.name, 'assignment_type': CourseCohort.RANDOM, 'group_id': 2}
|
||||
response_dict = self.put_cohort_handler(self.course, self.cohort1, data=data, expected_response_code=400)
|
||||
response_dict = self.put_handler(self.course, self.cohort1, data=data, expected_response_code=400)
|
||||
self.assertEqual(
|
||||
"If group_id is specified, user_partition_id must also be specified.", response_dict.get("error")
|
||||
)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
"""
|
||||
Views related to course groups functionality.
|
||||
"""
|
||||
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.contrib.auth.models import User
|
||||
@@ -12,6 +16,7 @@ from django.utils.translation import ugettext
|
||||
import logging
|
||||
import re
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from courseware.courses import get_course_with_access
|
||||
from edxmako.shortcuts import render_to_response
|
||||
@@ -55,6 +60,19 @@ def unlink_cohort_partition_group(cohort):
|
||||
CourseUserGroupPartitionGroup.objects.filter(course_user_group=cohort).delete()
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _get_course_cohort_settings_representation(course_cohort_settings):
|
||||
"""
|
||||
Returns a JSON representation of a course cohort settings.
|
||||
"""
|
||||
return {
|
||||
'id': course_cohort_settings.id,
|
||||
'is_cohorted': course_cohort_settings.is_cohorted,
|
||||
'cohorted_discussions': course_cohort_settings.cohorted_discussions,
|
||||
'always_cohort_inline_discussions': course_cohort_settings.always_cohort_inline_discussions,
|
||||
}
|
||||
|
||||
|
||||
def _get_cohort_representation(cohort, course):
|
||||
"""
|
||||
Returns a JSON representation of a cohort.
|
||||
@@ -71,6 +89,33 @@ def _get_cohort_representation(cohort, course):
|
||||
}
|
||||
|
||||
|
||||
@require_http_methods(("GET", "PUT", "POST"))
|
||||
@ensure_csrf_cookie
|
||||
@expect_json
|
||||
@login_required
|
||||
def course_cohort_settings_handler(request, course_key_string):
|
||||
"""
|
||||
The restful handler for cohort setting requests. Requires JSON.
|
||||
This will raise 404 if user is not staff.
|
||||
GET
|
||||
Returns the JSON representation of cohort settings for the course.
|
||||
PUT or POST
|
||||
Updates the cohort settings for the course. Returns the JSON representation of updated settings.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
get_course_with_access(request.user, 'staff', course_key)
|
||||
if request.method == 'GET':
|
||||
cohort_settings = cohorts.get_course_cohort_settings(course_key)
|
||||
return JsonResponse(_get_course_cohort_settings_representation(cohort_settings))
|
||||
else:
|
||||
is_cohorted = request.json.get('is_cohorted')
|
||||
if is_cohorted is None:
|
||||
# Note: error message not translated because it is not exposed to the user (UI prevents this state).
|
||||
return JsonResponse({"error": "Bad Request"}, 400)
|
||||
cohort_settings = cohorts.set_course_cohort_settings(course_key, is_cohorted=is_cohorted)
|
||||
return JsonResponse(_get_course_cohort_settings_representation(cohort_settings))
|
||||
|
||||
|
||||
@require_http_methods(("GET", "PUT", "POST", "PATCH"))
|
||||
@ensure_csrf_cookie
|
||||
@expect_json
|
||||
@@ -306,7 +351,7 @@ def debug_cohort_mgmt(request, course_key_string):
|
||||
# add staff check to make sure it's safe if it's accidentally deployed.
|
||||
get_course_with_access(request.user, 'staff', course_key)
|
||||
|
||||
context = {'cohorts_ajax_url': reverse(
|
||||
context = {'cohorts_url': reverse(
|
||||
'cohorts',
|
||||
kwargs={'course_key': course_key.to_deprecated_string()}
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user