Commit 3ce494f5 by Muhammad Ammar Committed by Usman Khalid

Enable/disable cohorts from the instructor dashboard and move cohorts management to its own tab

TNL-1268
parent d8396924
...@@ -28,6 +28,15 @@ class InstructorDashboardPage(CoursePage): ...@@ -28,6 +28,15 @@ class InstructorDashboardPage(CoursePage):
membership_section.wait_for_page() membership_section.wait_for_page()
return membership_section 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): def select_data_download(self):
""" """
Selects the data download tab and returns a DataDownloadPage. Selects the data download tab and returns a DataDownloadPage.
...@@ -84,16 +93,10 @@ class MembershipPage(PageObject): ...@@ -84,16 +93,10 @@ class MembershipPage(PageObject):
""" """
return MembershipPageAutoEnrollSection(self.browser) 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 url = None
csv_browse_button_selector_css = '.csv-upload #file-upload-form-file' csv_browse_button_selector_css = '.csv-upload #file-upload-form-file'
...@@ -104,13 +107,13 @@ class MembershipPageCohortManagementSection(PageObject): ...@@ -104,13 +107,13 @@ class MembershipPageCohortManagementSection(PageObject):
assignment_type_buttons_css = '.cohort-management-assignment-type-settings input' assignment_type_buttons_css = '.cohort-management-assignment-type-settings input'
def is_browser_on_page(self): 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): def _bounded_selector(self, selector):
""" """
Return `selector`, but limited to the cohort management context. 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): def _get_cohort_options(self):
""" """
...@@ -158,9 +161,9 @@ class MembershipPageCohortManagementSection(PageObject): ...@@ -158,9 +161,9 @@ class MembershipPageCohortManagementSection(PageObject):
Return assignment settings disabled message in case of default cohort. Return assignment settings disabled message in case of default cohort.
""" """
query = self.q(css=self._bounded_selector('.copy-error')) query = self.q(css=self._bounded_selector('.copy-error'))
if query.present: if query.visible:
return query.text[0] return query.text[0]
else:
return '' return ''
@property @property
...@@ -232,7 +235,11 @@ class MembershipPageCohortManagementSection(PageObject): ...@@ -232,7 +235,11 @@ class MembershipPageCohortManagementSection(PageObject):
Adds a new manual cohort with the specified name. 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. 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 # 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. # (in which case the first is not visible). Click on the last present create button.
create_buttons.results[len(create_buttons.results) - 1].click() create_buttons.results[len(create_buttons.results) - 1].click()
...@@ -444,6 +451,28 @@ class MembershipPageCohortManagementSection(PageObject): ...@@ -444,6 +451,28 @@ class MembershipPageCohortManagementSection(PageObject):
file_input.send_keys(path) file_input.send_keys(path)
self.q(css=self._bounded_selector(self.csv_upload_button_selector_css)).first.click() 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): class MembershipPageAutoEnrollSection(PageObject):
""" """
......
...@@ -63,8 +63,7 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin ...@@ -63,8 +63,7 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin
# go to the membership page on the instructor dashboard # go to the membership page on the instructor dashboard
self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
self.instructor_dashboard_page.visit() self.instructor_dashboard_page.visit()
membership_page = self.instructor_dashboard_page.select_membership() self.cohort_management_page = self.instructor_dashboard_page.select_cohort_management()
self.cohort_management_page = membership_page.select_cohort_management_section()
def verify_cohort_description(self, cohort_name, expected_description): def verify_cohort_description(self, cohort_name, expected_description):
""" """
...@@ -441,9 +440,31 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin ...@@ -441,9 +440,31 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin
self.assertTrue(self.cohort_management_page.is_assignment_settings_disabled) 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) 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): def test_link_to_data_download(self):
""" """
Scenario: a link is present from the cohort configuration in Scenario: a link is present from the cohort configuration in
...@@ -656,8 +677,7 @@ class CohortContentGroupAssociationTest(UniqueCourseTest, CohortTestMixin): ...@@ -656,8 +677,7 @@ class CohortContentGroupAssociationTest(UniqueCourseTest, CohortTestMixin):
# go to the membership page on the instructor dashboard # go to the membership page on the instructor dashboard
self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
self.instructor_dashboard_page.visit() self.instructor_dashboard_page.visit()
membership_page = self.instructor_dashboard_page.select_membership() self.cohort_management_page = self.instructor_dashboard_page.select_cohort_management()
self.cohort_management_page = membership_page.select_cohort_management_section()
def test_no_content_group_linked(self): def test_no_content_group_linked(self):
""" """
......
...@@ -154,8 +154,7 @@ class EndToEndCohortedCoursewareTest(ContainerBase): ...@@ -154,8 +154,7 @@ class EndToEndCohortedCoursewareTest(ContainerBase):
""" """
instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
instructor_dashboard_page.visit() instructor_dashboard_page.visit()
membership_page = instructor_dashboard_page.select_membership() cohort_management_page = instructor_dashboard_page.select_cohort_management()
cohort_management_page = membership_page.select_cohort_management_section()
def add_cohort_with_student(cohort_name, content_group, student): def add_cohort_with_student(cohort_name, content_group, student):
cohort_management_page.add_cohort(cohort_name, content_group=content_group) cohort_management_page.add_cohort(cohort_name, content_group=content_group)
......
...@@ -65,9 +65,7 @@ def instructor_dashboard_2(request, course_id): ...@@ -65,9 +65,7 @@ def instructor_dashboard_2(request, course_id):
'finance_admin': CourseFinanceAdminRole(course_key).has_user(request.user), 'finance_admin': CourseFinanceAdminRole(course_key).has_user(request.user),
'sales_admin': CourseSalesAdminRole(course_key).has_user(request.user), 'sales_admin': CourseSalesAdminRole(course_key).has_user(request.user),
'staff': has_access(request.user, 'staff', course), 'staff': has_access(request.user, 'staff', course),
'forum_admin': has_forum_access( 'forum_admin': has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR),
request.user, course_key, FORUM_ROLE_ADMINISTRATOR
),
} }
if not access['staff']: if not access['staff']:
...@@ -79,6 +77,7 @@ def instructor_dashboard_2(request, course_id): ...@@ -79,6 +77,7 @@ def instructor_dashboard_2(request, course_id):
_section_student_admin(course, access), _section_student_admin(course, access),
_section_data_download(course, access), _section_data_download(course, access),
_section_analytics(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 #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): ...@@ -330,7 +329,22 @@ def _section_membership(course, access):
'modify_access_url': reverse('modify_access', kwargs={'course_id': unicode(course_key)}), '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)}), '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)}), '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'), 'advanced_settings_url': get_studio_url(course, 'settings/advanced'),
'upload_cohorts_csv_url': reverse('add_users_to_cohorts', kwargs={'course_id': unicode(course_key)}), '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) -> ...@@ -176,6 +176,9 @@ setup_instructor_dashboard_sections = (idash_content) ->
, ,
constructor: window.InstructorDashboard.sections.Metrics constructor: window.InstructorDashboard.sections.Metrics
$element: idash_content.find ".#{CSS_IDASH_SECTION}#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}) -> sections_to_initialize.map ({constructor, $element}) ->
......
;(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);
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);
var edx = edx || {}; var edx = edx || {};
(function($, _, Backbone, gettext, interpolate_text, CohortModel, CohortEditorView, CohortFormView, (function($, _, Backbone, gettext, interpolate_text, CohortModel, CohortEditorView, CohortFormView,
NotificationModel, NotificationView, FileUploaderView) { CourseCohortSettingsNotificationView, NotificationModel, NotificationView, FileUploaderView) {
'use strict'; 'use strict';
var hiddenClass = 'is-hidden', var hiddenClass = 'is-hidden',
...@@ -12,6 +12,7 @@ var edx = edx || {}; ...@@ -12,6 +12,7 @@ var edx = edx || {};
edx.groups.CohortsView = Backbone.View.extend({ edx.groups.CohortsView = Backbone.View.extend({
events : { events : {
'change .cohort-select': 'onCohortSelected', 'change .cohort-select': 'onCohortSelected',
'change .cohorts-state': 'onCohortsEnabledChanged',
'click .action-create': 'showAddCohortForm', 'click .action-create': 'showAddCohortForm',
'click .cohort-management-add-form .action-save': 'saveAddCohortForm', 'click .cohort-management-add-form .action-save': 'saveAddCohortForm',
'click .cohort-management-add-form .action-cancel': 'cancelAddCohortForm', 'click .cohort-management-add-form .action-cancel': 'cancelAddCohortForm',
...@@ -26,19 +27,21 @@ var edx = edx || {}; ...@@ -26,19 +27,21 @@ var edx = edx || {};
this.selectorTemplate = _.template($('#cohort-selector-tpl').text()); this.selectorTemplate = _.template($('#cohort-selector-tpl').text());
this.context = options.context; this.context = options.context;
this.contentGroups = options.contentGroups; this.contentGroups = options.contentGroups;
this.cohortSettings = options.cohortSettings;
model.on('sync', this.onSync, this); 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 // (for example, after uploading a csv file of cohort assignments and then
// checking results on data download tab). // checking results on data download tab).
$(this.getSectionCss('membership')).click(function () { $(this.getSectionCss('cohort_management')).click(function () {
model.fetch(); model.fetch();
}); });
}, },
render: function() { render: function() {
this.$el.html(this.template({ this.$el.html(this.template({
cohorts: this.model.models cohorts: this.model.models,
cohortsEnabled: this.cohortSettings.get('is_cohorted')
})); }));
this.onSync(); this.onSync();
return this; return this;
...@@ -51,6 +54,13 @@ var edx = edx || {}; ...@@ -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) { onSync: function(model, response, options) {
var selectedCohort = this.lastSelectedCohortId && this.model.get(this.lastSelectedCohortId), var selectedCohort = this.lastSelectedCohortId && this.model.get(this.lastSelectedCohortId),
hasCohorts = this.model.length > 0, hasCohorts = this.model.length > 0,
...@@ -98,6 +108,28 @@ var edx = edx || {}; ...@@ -98,6 +108,28 @@ var edx = edx || {};
this.showCohortEditor(selectedCohort); 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) { showCohortEditor: function(cohort) {
this.removeNotification(); this.removeNotification();
if (this.editor) { if (this.editor) {
...@@ -242,4 +274,5 @@ var edx = edx || {}; ...@@ -242,4 +274,5 @@ var edx = edx || {};
} }
}); });
}).call(this, $, _, Backbone, gettext, interpolate_text, edx.groups.CohortModel, edx.groups.CohortEditorView, }).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);
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);
(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);
define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers', 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'], 'js/groups/views/cohorts', 'js/groups/collections/cohort', 'js/groups/models/content_group',
function (Backbone, $, AjaxHelpers, TemplateHelpers, CohortsView, CohortCollection, ContentGroupModel) { 'js/groups/models/course_cohort_settings', 'js/groups/views/course_cohort_settings_notification'],
function (Backbone, $, AjaxHelpers, TemplateHelpers, CohortsView, CohortCollection, ContentGroupModel,
CourseCohortSettingsModel, CourseCohortSettingsNotificationView) {
'use strict'; 'use strict';
describe("Cohorts View", function () { describe("Cohorts View", function () {
var catLoversInitialCount = 123, dogLoversInitialCount = 456, unknownUserMessage, var catLoversInitialCount = 123, dogLoversInitialCount = 456, unknownUserMessage,
createMockCohort, createMockCohorts, createMockContentGroups, createCohortsView, cohortsView, createMockCohort, createMockCohorts, createMockContentGroups, createCohortSettings, createCohortsView,
requests, respondToRefresh, verifyMessage, verifyNoMessage, verifyDetailedMessage, verifyHeader, cohortsView, requests, respondToRefresh, verifyMessage, verifyNoMessage, verifyDetailedMessage,
expectCohortAddRequest, getAddModal, selectContentGroup, clearContentGroup, saveFormAndExpectErrors, verifyHeader, expectCohortAddRequest, getAddModal, selectContentGroup, clearContentGroup,
MOCK_COHORTED_USER_PARTITION_ID, MOCK_UPLOAD_COHORTS_CSV_URL, MOCK_STUDIO_ADVANCED_SETTINGS_URL, saveFormAndExpectErrors, createMockCohortSettings, MOCK_COHORTED_USER_PARTITION_ID,
MOCK_STUDIO_GROUP_CONFIGURATIONS_URL, MOCK_MANUAL_ASSIGNMENT, MOCK_RANDOM_ASSIGNMENT; 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_MANUAL_ASSIGNMENT = 'manual';
MOCK_RANDOM_ASSIGNMENT = 'random'; MOCK_RANDOM_ASSIGNMENT = 'random';
...@@ -49,17 +52,35 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -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) { createCohortsView = function (test, options) {
var cohortsJson, cohorts, contentGroups; var cohortsJson, cohorts, contentGroups, cohortSettings;
options = options || {}; options = options || {};
cohortsJson = options.cohorts ? {cohorts: options.cohorts} : createMockCohorts(); cohortsJson = options.cohorts ? {cohorts: options.cohorts} : createMockCohorts();
cohorts = new CohortCollection(cohortsJson, {parse: true}); cohorts = new CohortCollection(cohortsJson, {parse: true});
contentGroups = options.contentGroups || createMockContentGroups(); contentGroups = options.contentGroups || createMockContentGroups();
cohortSettings = options.cohortSettings || createCohortSettings(true);
cohortSettings.url = '/mock_service/cohorts/settings';
cohorts.url = '/mock_service/cohorts'; cohorts.url = '/mock_service/cohorts';
requests = AjaxHelpers.requests(test); requests = AjaxHelpers.requests(test);
cohortsView = new CohortsView({ cohortsView = new CohortsView({
model: cohorts, model: cohorts,
contentGroups: contentGroups, contentGroups: contentGroups,
cohortSettings: cohortSettings,
context: { context: {
uploadCohortsCsvUrl: MOCK_UPLOAD_COHORTS_CSV_URL, uploadCohortsCsvUrl: MOCK_UPLOAD_COHORTS_CSV_URL,
studioAdvancedSettingsUrl: MOCK_STUDIO_ADVANCED_SETTINGS_URL, studioAdvancedSettingsUrl: MOCK_STUDIO_ADVANCED_SETTINGS_URL,
...@@ -177,13 +198,14 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -177,13 +198,14 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
}; };
beforeEach(function () { 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/cohorts');
TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-form'); 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-selector');
TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-editor'); 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/cohort-group-header');
TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/notification'); TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/notification');
TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-state');
TemplateHelpers.installTemplate('templates/file-upload'); TemplateHelpers.installTemplate('templates/file-upload');
}); });
...@@ -202,7 +224,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -202,7 +224,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
it("syncs data when membership tab is clicked", function() { it("syncs data when membership tab is clicked", function() {
createCohortsView(this, {selectCohort: 1}); createCohortsView(this, {selectCohort: 1});
verifyHeader(1, 'Cat Lovers', catLoversInitialCount); verifyHeader(1, 'Cat Lovers', catLoversInitialCount);
$(cohortsView.getSectionCss("membership")).click(); $(cohortsView.getSectionCss("cohort_management")).click();
AjaxHelpers.expectRequest(requests, 'GET', '/mock_service/cohorts'); AjaxHelpers.expectRequest(requests, 'GET', '/mock_service/cohorts');
respondToRefresh(1001, 2); respondToRefresh(1001, 2);
verifyHeader(1, 'Cat Lovers', 1001); verifyHeader(1, 'Cat Lovers', 1001);
...@@ -255,6 +277,54 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -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 () { describe("Cohort Group Header", function () {
it("renders header correctly", function () { it("renders header correctly", function () {
var cohortName = 'Transformers', var cohortName = 'Transformers',
...@@ -867,7 +937,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -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 // 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.$('.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() { it("cancel settings works", function() {
......
...@@ -67,6 +67,8 @@ ...@@ -67,6 +67,8 @@
'js/views/notification': 'js/views/notification', 'js/views/notification': 'js/views/notification',
'js/groups/models/cohort': 'js/groups/models/cohort', 'js/groups/models/cohort': 'js/groups/models/cohort',
'js/groups/models/content_group': 'js/groups/models/content_group', '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/collections/cohort': 'js/groups/collections/cohort',
'js/groups/views/cohort_editor': 'js/groups/views/cohort_editor', 'js/groups/views/cohort_editor': 'js/groups/views/cohort_editor',
'js/groups/views/cohort_form': 'js/groups/views/cohort_form', 'js/groups/views/cohort_form': 'js/groups/views/cohort_form',
...@@ -294,6 +296,14 @@ ...@@ -294,6 +296,14 @@
exports: 'edx.groups.ContentGroupModel', exports: 'edx.groups.ContentGroupModel',
deps: ['backbone'] 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': { 'js/groups/collections/cohort': {
exports: 'edx.groups.CohortCollection', exports: 'edx.groups.CohortCollection',
deps: ['backbone', 'js/groups/models/cohort'] deps: ['backbone', 'js/groups/models/cohort']
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
<%- gettext('Students in this cohort are:') %> <%- gettext('Students in this cohort are:') %>
</h4> </h4>
<label> <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>
<label> <label>
<input type="radio" class="type-manual" name="cohort-assignment-type" value="manual" <%- assignment_type == 'manual' || isNewCohort ? 'checked="checked"' : '' %>/> <%- gettext("Manually Assigned") %> <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 @@ ...@@ -47,7 +47,7 @@
<% if (isDefaultCohort) { %> <% if (isDefaultCohort) { %>
<p class="copy-error"> <p class="copy-error">
<i class="icon fa fa-exclamation-triangle" aria-hidden="true"></i> <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> </p>
<% } %> <% } %>
<hr class="divider divider-lv1"> <hr class="divider divider-lv1">
......
<div class="nav-utilities">
<span class="action-toggle-message" aria-live="polite"></span>
</div>
\ No newline at end of file
<%! 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>
<h2 class="section-title"> <div class="cohorts-state-section">
<span class="value"><%- gettext('Cohort Management') %></span> <label> <input type="checkbox" class="cohorts-state" value="Cohorts-State" <%- cohortsEnabled ? 'checked="checked"' : '' %> /> <%- gettext('Enable Cohorts') %></label>
<span class="description"></span> </div>
</h2>
<div class="cohort-management-nav"> <% if (cohortsEnabled) { %>
<h3 class="subsection-title"><%- gettext('Assign students to cohorts manually') %></h3> <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"> <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"> <div class="cohort-management-nav-form-select field field-select">
...@@ -21,15 +21,15 @@ ...@@ -21,15 +21,15 @@
<i class="icon fa fa-plus" aria-hidden="true"></i> <i class="icon fa fa-plus" aria-hidden="true"></i>
<%- gettext('Add Cohort') %> <%- gettext('Add Cohort') %>
</a> </a>
</div> </div>
<!-- Add modal --> <!-- Add modal -->
<div class="cohort-management-add-form"></div> <div class="cohort-management-add-form"></div>
<!-- individual group --> <!-- individual group -->
<div class="cohort-management-group"></div> <div class="cohort-management-group"></div>
<div class="wrapper-cohort-supplemental"> <div class="wrapper-cohort-supplemental">
<hr class="divider divider-lv1" /> <hr class="divider divider-lv1" />
...@@ -47,4 +47,5 @@ ...@@ -47,4 +47,5 @@
) %> ) %>
</p> </p>
</div> </div>
</div> </div>
<% } %>
...@@ -53,12 +53,16 @@ ...@@ -53,12 +53,16 @@
<%static:js group='application'/> <%static:js group='application'/>
## Backbone classes declared explicitly until RequireJS is supported ## 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/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/notification.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/file_uploader.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/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/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/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_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/cohort_editor.js')}"></script>
<script type="text/javascript" src="${static.url('js/groups/views/cohorts.js')}"></script> <script type="text/javascript" src="${static.url('js/groups/views/cohorts.js')}"></script>
...@@ -66,7 +70,7 @@ ...@@ -66,7 +70,7 @@
## Include Underscore templates ## Include Underscore templates
<%block name="header_extras"> <%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"> <script type="text/template" id="${template_name}-tpl">
<%static:include path="instructor/instructor_dashboard_2/${template_name}.underscore" /> <%static:include path="instructor/instructor_dashboard_2/${template_name}.underscore" />
</script> </script>
......
...@@ -245,53 +245,3 @@ ...@@ -245,53 +245,3 @@
%endif %endif
</div> </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: ...@@ -379,6 +379,9 @@ if settings.COURSEWARE_ENABLED:
'open_ended_grading.views.take_action_on_flags', name='open_ended_flagged_problems_take_action'), 'open_ended_grading.views.take_action_on_flags', name='open_ended_flagged_problems_take_action'),
# Cohorts management # 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), url(r'^courses/{}/cohorts/(?P<cohort_id>[0-9]+)?$'.format(settings.COURSE_KEY_PATTERN),
'openedx.core.djangoapps.course_groups.views.cohort_handler', name="cohorts"), 'openedx.core.djangoapps.course_groups.views.cohort_handler', name="cohorts"),
url(r'^courses/{}/cohorts/(?P<cohort_id>[0-9]+)$'.format(settings.COURSE_KEY_PATTERN), 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. ...@@ -5,7 +5,6 @@ forums, and to the cohort admin views.
import logging import logging
import random import random
import json
from django.db import transaction from django.db import transaction
from django.db.models.signals import post_save, m2m_changed from django.db.models.signals import post_save, m2m_changed
...@@ -238,7 +237,7 @@ def migrate_cohort_settings(course): ...@@ -238,7 +237,7 @@ def migrate_cohort_settings(course):
course_id=course_id, course_id=course_id,
defaults={ defaults={
'is_cohorted': course.is_cohorted, '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 'always_cohort_inline_discussions': course.always_cohort_inline_discussions
} }
) )
...@@ -324,7 +323,8 @@ def add_cohort(course_key, name, assignment_type): ...@@ -324,7 +323,8 @@ def add_cohort(course_key, name, assignment_type):
raise ValueError("Invalid course_key") raise ValueError("Invalid course_key")
cohort = CourseCohort.create( cohort = CourseCohort.create(
cohort_name=name, course_id=course.id, cohort_name=name,
course_id=course.id,
assignment_type=assignment_type assignment_type=assignment_type
).course_user_group ).course_user_group
...@@ -413,7 +413,7 @@ def set_assignment_type(user_group, assignment_type): ...@@ -413,7 +413,7 @@ def set_assignment_type(user_group, assignment_type):
course_cohort = user_group.cohort course_cohort = user_group.cohort
if is_default_cohort(user_group) and course_cohort.assignment_type != assignment_type: 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.assignment_type = assignment_type
course_cohort.save() course_cohort.save()
...@@ -438,3 +438,48 @@ def is_default_cohort(user_group): ...@@ -438,3 +438,48 @@ def is_default_cohort(user_group):
) )
return len(random_cohorts) == 1 and random_cohorts[0].name == user_group.name 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
# -*- 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']
\ No newline at end of file
"""
Django models related to course groups functionality.
"""
import json
import logging import logging
from django.contrib.auth.models import User from django.contrib.auth.models import User
...@@ -81,11 +86,21 @@ class CourseCohortsSettings(models.Model): ...@@ -81,11 +86,21 @@ class CourseCohortsSettings(models.Model):
help_text="Which course are these settings associated with?", 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 # pylint: disable=invalid-name
always_cohort_inline_discussions = models.BooleanField(default=True) 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): class CourseCohort(models.Model):
""" """
......
...@@ -8,7 +8,9 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey ...@@ -8,7 +8,9 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from ..models import CourseUserGroup, CourseCohort from ..models import CourseUserGroup, CourseCohort, CourseCohortsSettings
import json
class CohortFactory(DjangoModelFactory): class CohortFactory(DjangoModelFactory):
...@@ -40,6 +42,19 @@ class CourseCohortFactory(DjangoModelFactory): ...@@ -40,6 +42,19 @@ class CourseCohortFactory(DjangoModelFactory):
assignment_type = 'manual' 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): def topic_name_to_id(course, name):
""" """
Given a discussion topic name, return an id for that name (includes Given a discussion topic name, return an id for that name (includes
......
...@@ -3,7 +3,6 @@ Tests for cohorts ...@@ -3,7 +3,6 @@ Tests for cohorts
""" """
# pylint: disable=no-member # pylint: disable=no-member
from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import IntegrityError from django.db import IntegrityError
from django.http import Http404 from django.http import Http404
...@@ -19,8 +18,9 @@ from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTO ...@@ -19,8 +18,9 @@ from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTO
from ..models import CourseUserGroup, CourseCohort, CourseUserGroupPartitionGroup from ..models import CourseUserGroup, CourseCohort, CourseUserGroupPartitionGroup
from .. import cohorts 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") @patch("openedx.core.djangoapps.course_groups.cohorts.tracker")
class TestCohortSignals(TestCase): class TestCohortSignals(TestCase):
...@@ -209,7 +209,7 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -209,7 +209,7 @@ class TestCohorts(ModuleStoreTestCase):
self.assertEqual(cohorts.get_assignment_type(cohort), CourseCohort.RANDOM) 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: with self.assertRaises(ValueError) as context_manager:
cohorts.set_assignment_type(cohort, CourseCohort.MANUAL) cohorts.set_assignment_type(cohort, CourseCohort.MANUAL)
...@@ -685,12 +685,43 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -685,12 +685,43 @@ class TestCohorts(ModuleStoreTestCase):
lambda: cohorts.add_user_to_cohort(first_cohort, "non_existent_username") 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): class TestCohortsAndPartitionGroups(ModuleStoreTestCase):
MODULESTORE = TEST_DATA_MIXED_TOY_MODULESTORE
""" """
Test Cohorts and Partitions Groups. Test Cohorts and Partitions Groups.
""" """
MODULESTORE = TEST_DATA_MIXED_TOY_MODULESTORE
def setUp(self): def setUp(self):
""" """
......
"""
Views related to course groups functionality.
"""
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.contrib.auth.models import User from django.contrib.auth.models import User
...@@ -12,6 +16,7 @@ from django.utils.translation import ugettext ...@@ -12,6 +16,7 @@ from django.utils.translation import ugettext
import logging import logging
import re import re
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
...@@ -55,6 +60,19 @@ def unlink_cohort_partition_group(cohort): ...@@ -55,6 +60,19 @@ def unlink_cohort_partition_group(cohort):
CourseUserGroupPartitionGroup.objects.filter(course_user_group=cohort).delete() 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): def _get_cohort_representation(cohort, course):
""" """
Returns a JSON representation of a cohort. Returns a JSON representation of a cohort.
...@@ -71,6 +89,33 @@ def _get_cohort_representation(cohort, course): ...@@ -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")) @require_http_methods(("GET", "PUT", "POST", "PATCH"))
@ensure_csrf_cookie @ensure_csrf_cookie
@expect_json @expect_json
...@@ -306,7 +351,7 @@ def debug_cohort_mgmt(request, course_key_string): ...@@ -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. # add staff check to make sure it's safe if it's accidentally deployed.
get_course_with_access(request.user, 'staff', course_key) get_course_with_access(request.user, 'staff', course_key)
context = {'cohorts_ajax_url': reverse( context = {'cohorts_url': reverse(
'cohorts', 'cohorts',
kwargs={'course_key': course_key.to_deprecated_string()} kwargs={'course_key': course_key.to_deprecated_string()}
)} )}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment