Commit 0ebd8c29 by muhammad-ammar Committed by Usman Khalid

Allow Instructor To Rename Cohorts And Set Cohort Assignment Method

TNL-181
parent ec6cc04b
...@@ -6,7 +6,7 @@ Instructor (2) dashboard page. ...@@ -6,7 +6,7 @@ Instructor (2) dashboard page.
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from .course_page import CoursePage from .course_page import CoursePage
import os import os
from bok_choy.promise import EmptyPromise from bok_choy.promise import EmptyPromise, Promise
from ...tests.helpers import select_option_by_text, get_selected_option_text, get_options from ...tests.helpers import select_option_by_text, get_selected_option_text, get_options
...@@ -101,6 +101,7 @@ class MembershipPageCohortManagementSection(PageObject): ...@@ -101,6 +101,7 @@ class MembershipPageCohortManagementSection(PageObject):
content_group_selector_css = 'select.input-cohort-group-association' content_group_selector_css = 'select.input-cohort-group-association'
no_content_group_button_css = '.cohort-management-details-association-course input.radio-no' no_content_group_button_css = '.cohort-management-details-association-course input.radio-no'
select_content_group_button_css = '.cohort-management-details-association-course input.radio-yes' select_content_group_button_css = '.cohort-management-details-association-course input.radio-yes'
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.membership-section').present
...@@ -115,7 +116,12 @@ class MembershipPageCohortManagementSection(PageObject): ...@@ -115,7 +116,12 @@ class MembershipPageCohortManagementSection(PageObject):
""" """
Returns the available options in the cohort dropdown, including the initial "Select a cohort". Returns the available options in the cohort dropdown, including the initial "Select a cohort".
""" """
return self.q(css=self._bounded_selector("#cohort-select option")) def check_func():
"""Promise Check Function"""
query = self.q(css=self._bounded_selector("#cohort-select option"))
return len(query) > 0, query
return Promise(check_func, "Waiting for cohort selector to populate").fulfill()
def _cohort_name(self, label): def _cohort_name(self, label):
""" """
...@@ -129,6 +135,41 @@ class MembershipPageCohortManagementSection(PageObject): ...@@ -129,6 +135,41 @@ class MembershipPageCohortManagementSection(PageObject):
""" """
return int(label.split(' (')[1].split(')')[0]) return int(label.split(' (')[1].split(')')[0])
def save_cohort_settings(self):
"""
Click on Save button shown after click on Settings tab or when we add a new cohort.
"""
self.q(css=self._bounded_selector("div.form-actions .action-save")).first.click()
@property
def is_assignment_settings_disabled(self):
"""
Check if assignment settings are disabled.
"""
attributes = self.q(css=self._bounded_selector('.cohort-management-assignment-type-settings')).attrs('class')
if 'is-disabled' in attributes[0].split():
return True
return False
@property
def assignment_settings_message(self):
"""
Return assignment settings disabled message in case of default cohort.
"""
query = self.q(css=self._bounded_selector('.copy-error'))
if query.present:
return query.text[0]
else:
return ''
@property
def cohort_name_in_header(self):
"""
Return cohort name as shown in cohort header.
"""
return self._cohort_name(self.q(css=self._bounded_selector(".group-header-title .title-value")).text[0])
def get_cohorts(self): def get_cohorts(self):
""" """
Returns, as a list, the names of the available cohorts in the drop-down, filtering out "Select a cohort". Returns, as a list, the names of the available cohorts in the drop-down, filtering out "Select a cohort".
...@@ -142,10 +183,6 @@ class MembershipPageCohortManagementSection(PageObject): ...@@ -142,10 +183,6 @@ class MembershipPageCohortManagementSection(PageObject):
""" """
Returns the name of the selected cohort. Returns the name of the selected cohort.
""" """
EmptyPromise(
lambda: len(self._get_cohort_options().results) > 0,
"Waiting for cohort selector to populate"
).fulfill()
return self._cohort_name( return self._cohort_name(
self._get_cohort_options().filter(lambda el: el.is_selected()).first.text[0] self._get_cohort_options().filter(lambda el: el.is_selected()).first.text[0]
) )
...@@ -162,10 +199,6 @@ class MembershipPageCohortManagementSection(PageObject): ...@@ -162,10 +199,6 @@ class MembershipPageCohortManagementSection(PageObject):
""" """
Selects the given cohort in the drop-down. Selects the given cohort in the drop-down.
""" """
EmptyPromise(
lambda: cohort_name in self.get_cohorts(),
"Waiting for cohort selector to populate"
).fulfill()
# Note: can't use Select to select by text because the count is also included in the displayed text. # Note: can't use Select to select by text because the count is also included in the displayed text.
self._get_cohort_options().filter( self._get_cohort_options().filter(
lambda el: self._cohort_name(el.text) == cohort_name lambda el: self._cohort_name(el.text) == cohort_name
...@@ -176,7 +209,25 @@ class MembershipPageCohortManagementSection(PageObject): ...@@ -176,7 +209,25 @@ class MembershipPageCohortManagementSection(PageObject):
"Waiting to confirm cohort has been selected" "Waiting to confirm cohort has been selected"
).fulfill() ).fulfill()
def add_cohort(self, cohort_name, content_group=None): def set_cohort_name(self, cohort_name):
"""
Set Cohort Name.
"""
textinput = self.q(css=self._bounded_selector("#cohort-name")).results[0]
textinput.clear()
textinput.send_keys(cohort_name)
def set_assignment_type(self, assignment_type):
"""
Set assignment type for selected cohort.
Arguments:
assignment_type (str): Should be 'random' or 'manual'
"""
css = self._bounded_selector(self.assignment_type_buttons_css)
self.q(css=css).filter(lambda el: el.get_attribute('value') == assignment_type).first.click()
def add_cohort(self, cohort_name, content_group=None, assignment_type=None):
""" """
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.
...@@ -187,9 +238,15 @@ class MembershipPageCohortManagementSection(PageObject): ...@@ -187,9 +238,15 @@ class MembershipPageCohortManagementSection(PageObject):
create_buttons.results[len(create_buttons.results) - 1].click() create_buttons.results[len(create_buttons.results) - 1].click()
textinput = self.q(css=self._bounded_selector("#cohort-name")).results[0] textinput = self.q(css=self._bounded_selector("#cohort-name")).results[0]
textinput.send_keys(cohort_name) textinput.send_keys(cohort_name)
# Manual assignment type will be selected by default for a new cohort
# if we are not setting the assignment type explicitly
if assignment_type:
self.set_assignment_type(assignment_type)
if content_group: if content_group:
self._select_associated_content_group(content_group) self._select_associated_content_group(content_group)
self.q(css=self._bounded_selector("div.form-actions .action-save")).first.click() self.save_cohort_settings()
def get_cohort_group_setup(self): def get_cohort_group_setup(self):
""" """
...@@ -200,6 +257,12 @@ class MembershipPageCohortManagementSection(PageObject): ...@@ -200,6 +257,12 @@ class MembershipPageCohortManagementSection(PageObject):
def select_edit_settings(self): def select_edit_settings(self):
self.q(css=self._bounded_selector(".action-edit")).first.click() self.q(css=self._bounded_selector(".action-edit")).first.click()
def select_manage_settings(self):
"""
Click on Manage Students Tab under cohort management section.
"""
self.q(css=self._bounded_selector(".tab-manage_students")).first.click()
def add_students_to_selected_cohort(self, users): def add_students_to_selected_cohort(self, users):
""" """
Adds a list of users (either usernames or email addresses) to the currently selected cohort. Adds a list of users (either usernames or email addresses) to the currently selected cohort.
...@@ -245,23 +308,6 @@ class MembershipPageCohortManagementSection(PageObject): ...@@ -245,23 +308,6 @@ class MembershipPageCohortManagementSection(PageObject):
return None return None
return get_selected_option_text(self.q(css=self._bounded_selector(self.content_group_selector_css))) return get_selected_option_text(self.q(css=self._bounded_selector(self.content_group_selector_css)))
def verify_cohort_content_group_selected(self, content_group=None):
"""
Waits for the expected content_group (or none) to show as selected for
cohort associated content group.
"""
if content_group:
self.wait_for(
lambda: unicode(
self.q(css='select.input-cohort-group-association option:checked').text[0]
) == content_group,
"Cohort group has been selected."
)
else:
self.wait_for_element_visibility(
'.cohort-management-details-association-course input.radio-no:checked',
'Radio button "No content group" has been selected.')
def set_cohort_associated_content_group(self, content_group=None, select_settings=True): def set_cohort_associated_content_group(self, content_group=None, select_settings=True):
""" """
Sets the content group associated with the cohort currently being edited. Sets the content group associated with the cohort currently being edited.
...@@ -274,8 +320,7 @@ class MembershipPageCohortManagementSection(PageObject): ...@@ -274,8 +320,7 @@ class MembershipPageCohortManagementSection(PageObject):
self.q(css=self._bounded_selector(self.no_content_group_button_css)).first.click() self.q(css=self._bounded_selector(self.no_content_group_button_css)).first.click()
else: else:
self._select_associated_content_group(content_group) self._select_associated_content_group(content_group)
self.q(css=self._bounded_selector("div.form-actions .action-save")).first.click() self.save_cohort_settings()
self.verify_cohort_content_group_selected(content_group)
def _select_associated_content_group(self, content_group): def _select_associated_content_group(self, content_group):
""" """
......
...@@ -73,7 +73,7 @@ class CohortTestMixin(object): ...@@ -73,7 +73,7 @@ class CohortTestMixin(object):
Adds a cohort by name, returning its ID. Adds a cohort by name, returning its ID.
""" """
url = LMS_BASE_URL + "/courses/" + course_fixture._course_key + '/cohorts/' url = LMS_BASE_URL + "/courses/" + course_fixture._course_key + '/cohorts/'
data = json.dumps({"name": cohort_name}) data = json.dumps({"name": cohort_name, 'assignment_type': 'manual'})
response = course_fixture.session.post(url, data=data, headers=course_fixture.headers) response = course_fixture.session.post(url, data=data, headers=course_fixture.headers)
self.assertTrue(response.ok, "Failed to create cohort") self.assertTrue(response.ok, "Failed to create cohort")
return response.json()['id'] return response.json()['id']
......
...@@ -250,36 +250,47 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin ...@@ -250,36 +250,47 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin
self.cohort_management_page.get_cohort_student_input_field_value() self.cohort_management_page.get_cohort_student_input_field_value()
) )
def test_add_new_cohort(self): def _verify_cohort_settings(
""" self,
Scenario: A new manual cohort can be created, and a student assigned to it. cohort_name,
assignment_type=None,
new_cohort_name=None,
new_assignment_type=None,
verify_updated=False
):
Given I have a course with a user in the course """
When I add a new manual cohort to the course via the LMS instructor dashboard Create a new cohort and verify the new and existing settings.
Then the new cohort is displayed and has no users in it
And when I add the user to the new cohort
Then the cohort has 1 user
And appropriate events have been emitted
""" """
start_time = datetime.now(UTC) start_time = datetime.now(UTC)
new_cohort = str(uuid.uuid4().get_hex()[0:20]) self.assertFalse(cohort_name in self.cohort_management_page.get_cohorts())
self.assertFalse(new_cohort in self.cohort_management_page.get_cohorts()) self.cohort_management_page.add_cohort(cohort_name, assignment_type=assignment_type)
self.cohort_management_page.add_cohort(new_cohort)
# After adding the cohort, it should automatically be selected # After adding the cohort, it should automatically be selected
EmptyPromise( EmptyPromise(
lambda: new_cohort == self.cohort_management_page.get_selected_cohort(), "Waiting for new cohort to appear" lambda: cohort_name == self.cohort_management_page.get_selected_cohort(), "Waiting for new cohort to appear"
).fulfill() ).fulfill()
self.assertEqual(0, self.cohort_management_page.get_selected_cohort_count()) self.assertEqual(0, self.cohort_management_page.get_selected_cohort_count())
# After adding the cohort, it should automatically be selected and its
# assignment_type should be "manual" as this is the default assignment type
_assignment_type = assignment_type or 'manual'
msg = "Waiting for currently selected cohort assignment type"
EmptyPromise(
lambda: _assignment_type == self.cohort_management_page.get_cohort_associated_assignment_type(), msg
).fulfill()
# Go back to Manage Students Tab
self.cohort_management_page.select_manage_settings()
self.cohort_management_page.add_students_to_selected_cohort([self.instructor_name]) self.cohort_management_page.add_students_to_selected_cohort([self.instructor_name])
# Wait for the number of users in the cohort to change, indicating that the add operation is complete. # Wait for the number of users in the cohort to change, indicating that the add operation is complete.
EmptyPromise( EmptyPromise(
lambda: 1 == self.cohort_management_page.get_selected_cohort_count(), 'Waiting for student to be added' lambda: 1 == self.cohort_management_page.get_selected_cohort_count(), 'Waiting for student to be added'
).fulfill() ).fulfill()
self.assertFalse(self.cohort_management_page.is_assignment_settings_disabled)
self.assertEqual('', self.cohort_management_page.assignment_settings_message)
self.assertEqual( self.assertEqual(
self.event_collection.find({ self.event_collection.find({
"name": "edx.cohort.created", "name": "edx.cohort.created",
"time": {"$gt": start_time}, "time": {"$gt": start_time},
"event.cohort_name": new_cohort, "event.cohort_name": cohort_name,
}).count(), }).count(),
1 1
) )
...@@ -287,11 +298,152 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin ...@@ -287,11 +298,152 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin
self.event_collection.find({ self.event_collection.find({
"name": "edx.cohort.creation_requested", "name": "edx.cohort.creation_requested",
"time": {"$gt": start_time}, "time": {"$gt": start_time},
"event.cohort_name": new_cohort, "event.cohort_name": cohort_name,
}).count(), }).count(),
1 1
) )
if verify_updated:
self.cohort_management_page.select_cohort(cohort_name)
self.cohort_management_page.select_cohort_settings()
self.cohort_management_page.set_cohort_name(new_cohort_name)
self.cohort_management_page.set_assignment_type(new_assignment_type)
self.cohort_management_page.save_cohort_settings()
# If cohort name is empty, then we should get/see an error message.
if not new_cohort_name:
confirmation_messages = self.cohort_management_page.get_cohort_settings_messages(type='error')
self.assertEqual(
["The cohort cannot be saved", "You must specify a name for the cohort"],
confirmation_messages
)
else:
confirmation_messages = self.cohort_management_page.get_cohort_settings_messages()
self.assertEqual(["Saved cohort"], confirmation_messages)
self.assertEqual(new_cohort_name, self.cohort_management_page.cohort_name_in_header)
self.assertTrue(new_cohort_name in self.cohort_management_page.get_cohorts())
self.assertEqual(1, self.cohort_management_page.get_selected_cohort_count())
self.assertEqual(
new_assignment_type,
self.cohort_management_page.get_cohort_associated_assignment_type()
)
def test_add_new_cohort(self):
"""
Scenario: A new manual cohort can be created, and a student assigned to it.
Given I have a course with a user in the course
When I add a new manual cohort to the course via the LMS instructor dashboard
Then the new cohort is displayed and has no users in it
And assignment type of displayed cohort to "manual" because this is the default
And when I add the user to the new cohort
Then the cohort has 1 user
And appropriate events have been emitted
"""
cohort_name = str(uuid.uuid4().get_hex()[0:20])
self._verify_cohort_settings(cohort_name=cohort_name, assignment_type=None)
def test_add_new_cohort_with_manual_assignment_type(self):
"""
Scenario: A new cohort with manual assignment type can be created, and a student assigned to it.
Given I have a course with a user in the course
When I add a new manual cohort with manual assignment type to the course via the LMS instructor dashboard
Then the new cohort is displayed and has no users in it
And assignment type of displayed cohort is "manual"
And when I add the user to the new cohort
Then the cohort has 1 user
And appropriate events have been emitted
"""
cohort_name = str(uuid.uuid4().get_hex()[0:20])
self._verify_cohort_settings(cohort_name=cohort_name, assignment_type='manual')
def test_add_new_cohort_with_random_assignment_type(self):
"""
Scenario: A new cohort with random assignment type can be created, and a student assigned to it.
Given I have a course with a user in the course
When I add a new manual cohort with random assignment type to the course via the LMS instructor dashboard
Then the new cohort is displayed and has no users in it
And assignment type of displayed cohort is "random"
And when I add the user to the new cohort
Then the cohort has 1 user
And appropriate events have been emitted
"""
cohort_name = str(uuid.uuid4().get_hex()[0:20])
self._verify_cohort_settings(cohort_name=cohort_name, assignment_type='random')
def test_update_existing_cohort_settings(self):
"""
Scenario: Update existing cohort settings(cohort name, assignment type)
Given I have a course with a user in the course
When I add a new cohort with random assignment type to the course via the LMS instructor dashboard
Then the new cohort is displayed and has no users in it
And assignment type of displayed cohort is "random"
And when I add the user to the new cohort
Then the cohort has 1 user
And appropriate events have been emitted
Then I select the cohort (that you just created) from existing cohorts
Then I change its name and assignment type set to "manual"
Then I Save the settings
And cohort with new name is present in cohorts dropdown list
And cohort assignment type should be "manual"
"""
cohort_name = str(uuid.uuid4().get_hex()[0:20])
new_cohort_name = '{old}__NEW'.format(old=cohort_name)
self._verify_cohort_settings(
cohort_name=cohort_name,
assignment_type='random',
new_cohort_name=new_cohort_name,
new_assignment_type='manual',
verify_updated=True
)
def test_update_existing_cohort_settings_with_empty_cohort_name(self):
"""
Scenario: Update existing cohort settings(cohort name, assignment type).
Given I have a course with a user in the course
When I add a new cohort with random assignment type to the course via the LMS instructor dashboard
Then the new cohort is displayed and has no users in it
And assignment type of displayed cohort is "random"
And when I add the user to the new cohort
Then the cohort has 1 user
And appropriate events have been emitted
Then I select a cohort from existing cohorts
Then I set its name as empty string and assignment type set to "manual"
And I click on Save button
Then I should see an error message
"""
cohort_name = str(uuid.uuid4().get_hex()[0:20])
new_cohort_name = ''
self._verify_cohort_settings(
cohort_name=cohort_name,
assignment_type='random',
new_cohort_name=new_cohort_name,
new_assignment_type='manual',
verify_updated=True
)
def test_default_cohort_assignment_settings(self):
"""
Scenario: Cohort assignment settings are disabled for default cohort.
Given I have a course with a user in the course
And I have added a manual cohort
And I have added a random cohort
When I select the random cohort
Then cohort assignment settings are disabled
"""
self.cohort_management_page.select_cohort("AutoCohort1")
self.cohort_management_page.select_cohort_settings()
self.assertTrue(self.cohort_management_page.is_assignment_settings_disabled)
message = "There must be one cohort to which students can be randomly assigned."
self.assertEqual(message, self.cohort_management_page.assignment_settings_message)
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
......
...@@ -6,14 +6,17 @@ var edx = edx || {}; ...@@ -6,14 +6,17 @@ var edx = edx || {};
edx.groups = edx.groups || {}; edx.groups = edx.groups || {};
edx.groups.CohortEditorView = Backbone.View.extend({ edx.groups.CohortEditorView = Backbone.View.extend({
events : { events : {
'click .wrapper-tabs .tab': 'selectTab', 'click .wrapper-tabs .tab': 'selectTab',
'click .tab-content-settings .action-save': 'saveSettings', 'click .tab-content-settings .action-save': 'saveSettings',
'click .tab-content-settings .action-cancel': 'cancelSettings',
'submit .cohort-management-group-add-form': 'addStudents' 'submit .cohort-management-group-add-form': 'addStudents'
}, },
initialize: function(options) { initialize: function(options) {
this.template = _.template($('#cohort-editor-tpl').text()); this.template = _.template($('#cohort-editor-tpl').text());
this.groupHeaderTemplate = _.template($('#cohort-group-header-tpl').text());
this.cohorts = options.cohorts; this.cohorts = options.cohorts;
this.contentGroups = options.contentGroups; this.contentGroups = options.contentGroups;
this.context = options.context; this.context = options.context;
...@@ -26,9 +29,9 @@ var edx = edx || {}; ...@@ -26,9 +29,9 @@ var edx = edx || {};
render: function() { render: function() {
this.$el.html(this.template({ this.$el.html(this.template({
cohort: this.model, cohort: this.model
studioAdvancedSettingsUrl: this.context.studioAdvancedSettingsUrl
})); }));
this.renderGroupHeader();
this.cohortFormView = new CohortFormView({ this.cohortFormView = new CohortFormView({
model: this.model, model: this.model,
contentGroups: this.contentGroups, contentGroups: this.contentGroups,
...@@ -39,6 +42,13 @@ var edx = edx || {}; ...@@ -39,6 +42,13 @@ var edx = edx || {};
return this; return this;
}, },
renderGroupHeader: function() {
this.$('.cohort-management-group-header').html(this.groupHeaderTemplate({
cohort: this.model,
studioAdvancedSettingsUrl: this.context.studioAdvancedSettingsUrl
}));
},
selectTab: function(event) { selectTab: function(event) {
var tabElement = $(event.currentTarget), var tabElement = $(event.currentTarget),
tabName = tabElement.data('tab'); tabName = tabElement.data('tab');
...@@ -53,13 +63,20 @@ var edx = edx || {}; ...@@ -53,13 +63,20 @@ var edx = edx || {};
saveSettings: function(event) { saveSettings: function(event) {
var cohortFormView = this.cohortFormView; var cohortFormView = this.cohortFormView;
var self = this;
event.preventDefault(); event.preventDefault();
cohortFormView.saveForm() cohortFormView.saveForm()
.done(function() { .done(function() {
self.renderGroupHeader();
cohortFormView.showMessage(gettext('Saved cohort')); cohortFormView.showMessage(gettext('Saved cohort'));
}); });
}, },
cancelSettings: function(event) {
event.preventDefault();
this.render();
},
setCohort: function(cohort) { setCohort: function(cohort) {
this.model = cohort; this.model = cohort;
this.render(); this.render();
......
...@@ -35,12 +35,22 @@ var edx = edx || {}; ...@@ -35,12 +35,22 @@ var edx = edx || {};
render: function() { render: function() {
this.$el.html(this.template({ this.$el.html(this.template({
cohort: this.model, cohort: this.model,
isDefaultCohort: this.isDefault(this.model.get('name')),
contentGroups: this.contentGroups, contentGroups: this.contentGroups,
studioGroupConfigurationsUrl: this.context.studioGroupConfigurationsUrl studioGroupConfigurationsUrl: this.context.studioGroupConfigurationsUrl
})); }));
return this; return this;
}, },
isDefault: function(name) {
var cohorts = this.model.collection;
if (_.isUndefined(cohorts)) {
return false;
}
var randomModels = cohorts.where({assignment_type:'random'});
return (randomModels.length === 1) && (randomModels[0].get('name') === name);
},
onRadioButtonChange: function(event) { onRadioButtonChange: function(event) {
var target = $(event.currentTarget), var target = $(event.currentTarget),
groupsEnabled = target.val() === 'yes'; groupsEnabled = target.val() === 'yes';
...@@ -77,7 +87,11 @@ var edx = edx || {}; ...@@ -77,7 +87,11 @@ var edx = edx || {};
getUpdatedCohortName: function() { getUpdatedCohortName: function() {
var cohortName = this.$('.cohort-name').val(); var cohortName = this.$('.cohort-name').val();
return cohortName ? cohortName.trim() : this.model.get('name'); return cohortName ? cohortName.trim() : '';
},
getAssignmentType: function() {
return this.$('input[name="cohort-assignment-type"]:checked').val();
}, },
showMessage: function(message, type, details) { showMessage: function(message, type, details) {
...@@ -109,18 +123,21 @@ var edx = edx || {}; ...@@ -109,18 +123,21 @@ var edx = edx || {};
cohort = this.model, cohort = this.model,
saveOperation = $.Deferred(), saveOperation = $.Deferred(),
isUpdate = !_.isUndefined(this.model.id), isUpdate = !_.isUndefined(this.model.id),
fieldData, selectedContentGroup, errorMessages, showErrorMessage; fieldData, selectedContentGroup, selectedAssignmentType, errorMessages, showErrorMessage;
showErrorMessage = function(message, details) { showErrorMessage = function(message, details) {
self.showMessage(message, 'error', details); self.showMessage(message, 'error', details);
}; };
this.removeNotification(); this.removeNotification();
selectedContentGroup = this.getSelectedContentGroup(); selectedContentGroup = this.getSelectedContentGroup();
selectedAssignmentType = this.getAssignmentType();
fieldData = { fieldData = {
name: this.getUpdatedCohortName(), name: this.getUpdatedCohortName(),
group_id: selectedContentGroup ? selectedContentGroup.id : null, group_id: selectedContentGroup ? selectedContentGroup.id : null,
user_partition_id: selectedContentGroup ? selectedContentGroup.get('user_partition_id') : null user_partition_id: selectedContentGroup ? selectedContentGroup.get('user_partition_id') : null,
assignment_type: selectedAssignmentType
}; };
errorMessages = this.validate(fieldData); errorMessages = this.validate(fieldData);
if (errorMessages.length > 0) { if (errorMessages.length > 0) {
showErrorMessage( showErrorMessage(
isUpdate ? gettext("The cohort cannot be saved") : gettext("The cohort cannot be added"), isUpdate ? gettext("The cohort cannot be saved") : gettext("The cohort cannot be added"),
...@@ -129,7 +146,7 @@ var edx = edx || {}; ...@@ -129,7 +146,7 @@ var edx = edx || {};
saveOperation.reject(); saveOperation.reject();
} else { } else {
cohort.save( cohort.save(
fieldData, {patch: isUpdate} fieldData, {patch: isUpdate, wait: true}
).done(function(result) { ).done(function(result) {
cohort.id = result.id; cohort.id = result.id;
self.render(); // re-render to remove any now invalid error messages self.render(); // re-render to remove any now invalid error messages
......
...@@ -9,17 +9,20 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -9,17 +9,20 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
requests, respondToRefresh, verifyMessage, verifyNoMessage, verifyDetailedMessage, verifyHeader, requests, respondToRefresh, verifyMessage, verifyNoMessage, verifyDetailedMessage, verifyHeader,
expectCohortAddRequest, getAddModal, selectContentGroup, clearContentGroup, saveFormAndExpectErrors, expectCohortAddRequest, getAddModal, selectContentGroup, clearContentGroup, saveFormAndExpectErrors,
MOCK_COHORTED_USER_PARTITION_ID, MOCK_UPLOAD_COHORTS_CSV_URL, MOCK_STUDIO_ADVANCED_SETTINGS_URL, MOCK_COHORTED_USER_PARTITION_ID, MOCK_UPLOAD_COHORTS_CSV_URL, MOCK_STUDIO_ADVANCED_SETTINGS_URL,
MOCK_STUDIO_GROUP_CONFIGURATIONS_URL; MOCK_STUDIO_GROUP_CONFIGURATIONS_URL, MOCK_MANUAL_ASSIGNMENT, MOCK_RANDOM_ASSIGNMENT;
MOCK_MANUAL_ASSIGNMENT = 'manual';
MOCK_RANDOM_ASSIGNMENT = 'random';
MOCK_COHORTED_USER_PARTITION_ID = 0; MOCK_COHORTED_USER_PARTITION_ID = 0;
MOCK_UPLOAD_COHORTS_CSV_URL = 'http://upload-csv-file-url/'; MOCK_UPLOAD_COHORTS_CSV_URL = 'http://upload-csv-file-url/';
MOCK_STUDIO_ADVANCED_SETTINGS_URL = 'http://studio/settings/advanced'; MOCK_STUDIO_ADVANCED_SETTINGS_URL = 'http://studio/settings/advanced';
MOCK_STUDIO_GROUP_CONFIGURATIONS_URL = 'http://studio/group_configurations'; MOCK_STUDIO_GROUP_CONFIGURATIONS_URL = 'http://studio/group_configurations';
createMockCohort = function (name, id, userCount, groupId, userPartitionId) { createMockCohort = function (name, id, userCount, groupId, userPartitionId, assignmentType) {
return { return {
id: id !== undefined ? id : 1, id: id !== undefined ? id : 1,
name: name, name: name,
assignment_type: assignmentType || MOCK_MANUAL_ASSIGNMENT,
user_count: userCount !== undefined ? userCount : 0, user_count: userCount !== undefined ? userCount : 0,
group_id: groupId, group_id: groupId,
user_partition_id: userPartitionId user_partition_id: userPartitionId
...@@ -73,13 +76,13 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -73,13 +76,13 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
AjaxHelpers.respondWithJson(requests, createMockCohorts(catCount, dogCount)); AjaxHelpers.respondWithJson(requests, createMockCohorts(catCount, dogCount));
}; };
expectCohortAddRequest = function(name, groupId, userPartitionId) { expectCohortAddRequest = function(name, groupId, userPartitionId, assignmentType) {
AjaxHelpers.expectJsonRequest( AjaxHelpers.expectJsonRequest(
requests, 'POST', '/mock_service/cohorts', requests, 'POST', '/mock_service/cohorts',
{ {
name: name, name: name,
user_count: 0, user_count: 0,
assignment_type: '', assignment_type: assignmentType,
group_id: groupId, group_id: groupId,
user_partition_id: userPartitionId user_partition_id: userPartitionId
} }
...@@ -130,7 +133,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -130,7 +133,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
}); });
}; };
verifyHeader = function(expectedCohortId, expectedTitle, expectedCount) { verifyHeader = function(expectedCohortId, expectedTitle, expectedCount, assignmentType) {
var header = cohortsView.$('.cohort-management-group-header'); var header = cohortsView.$('.cohort-management-group-header');
expect(cohortsView.$('.cohort-select').val()).toBe(expectedCohortId.toString()); expect(cohortsView.$('.cohort-select').val()).toBe(expectedCohortId.toString());
expect(cohortsView.$('.cohort-select option:selected').text()).toBe( expect(cohortsView.$('.cohort-select option:selected').text()).toBe(
...@@ -147,6 +150,11 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -147,6 +150,11 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
{count: expectedCount} {count: expectedCount}
) )
); );
assignmentType = assignmentType || MOCK_MANUAL_ASSIGNMENT;
var manualMessage = "Students are added to this cohort only when you provide their email addresses or usernames on this page.";
var randomMessage = "Students are added to this cohort automatically.";
var message = (assignmentType == MOCK_MANUAL_ASSIGNMENT) ? manualMessage : randomMessage;
expect(header.find('.cohort-management-group-setup .setup-value').text().trim().split('\n')[0]).toBe(message);
}; };
saveFormAndExpectErrors = function(action, errors) { saveFormAndExpectErrors = function(action, errors) {
...@@ -174,6 +182,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -174,6 +182,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
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/notification'); TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/notification');
TemplateHelpers.installTemplate('templates/file-upload'); TemplateHelpers.installTemplate('templates/file-upload');
}); });
...@@ -246,6 +255,56 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -246,6 +255,56 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
}); });
}); });
describe("Cohort Group Header", function () {
it("renders header correctly", function () {
var cohortName = 'Transformers',
newCohortName = 'X Men';
var expectedRequest = function(assignment_type) {
return {
name: newCohortName,
assignment_type: assignment_type,
group_id: null,
user_partition_id: null
}
};
createCohortsView(this, {
cohorts: [
{
id: 1,
name: cohortName,
assignment_type: MOCK_RANDOM_ASSIGNMENT,
group_id: 111,
user_partition_id: MOCK_COHORTED_USER_PARTITION_ID
}
],
selectCohort: 1
});
// Select settings tab
cohortsView.$('.tab-settings a').click();
verifyHeader(1, cohortName, 0, MOCK_RANDOM_ASSIGNMENT);
// Update existing cohort values
cohortsView.$('.cohort-name').val(newCohortName);
cohortsView.$('.type-manual').prop('checked', true).change();
clearContentGroup();
// Save the updated settings
cohortsView.$('.action-save').click();
AjaxHelpers.expectJsonRequest(
requests, 'PATCH', '/mock_service/cohorts/1',
expectedRequest(MOCK_MANUAL_ASSIGNMENT)
);
AjaxHelpers.respondWithJson(
requests,
createMockCohort(newCohortName, 1, 0, null, null)
);
verifyHeader(1, newCohortName, 0, MOCK_MANUAL_ASSIGNMENT);
});
});
describe("Cohort Editor Tab Panel", function () { describe("Cohort Editor Tab Panel", function () {
it("initially selects the Manage Students tab", function () { it("initially selects the Manage Students tab", function () {
createCohortsView(this, {selectCohort: 1}); createCohortsView(this, {selectCohort: 1});
...@@ -267,6 +326,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -267,6 +326,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
describe("Add Cohorts Form", function () { describe("Add Cohorts Form", function () {
var defaultCohortName = 'New Cohort'; var defaultCohortName = 'New Cohort';
var assignmentType = 'random';
it("can add a cohort", function() { it("can add a cohort", function() {
var contentGroupId = 0, var contentGroupId = 0,
...@@ -277,39 +337,65 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -277,39 +337,65 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
expect(cohortsView.$('.cohort-management-nav')).toHaveClass('is-disabled'); expect(cohortsView.$('.cohort-management-nav')).toHaveClass('is-disabled');
expect(cohortsView.$('.cohort-management-group')).toHaveClass('is-hidden'); expect(cohortsView.$('.cohort-management-group')).toHaveClass('is-hidden');
cohortsView.$('.cohort-name').val(defaultCohortName); cohortsView.$('.cohort-name').val(defaultCohortName);
cohortsView.$('.type-random').prop('checked', true).change();
selectContentGroup(contentGroupId, MOCK_COHORTED_USER_PARTITION_ID); selectContentGroup(contentGroupId, MOCK_COHORTED_USER_PARTITION_ID);
cohortsView.$('.action-save').click(); cohortsView.$('.action-save').click();
expectCohortAddRequest(defaultCohortName, contentGroupId, MOCK_COHORTED_USER_PARTITION_ID); expectCohortAddRequest(defaultCohortName, contentGroupId, MOCK_COHORTED_USER_PARTITION_ID, assignmentType);
AjaxHelpers.respondWithJson( AjaxHelpers.respondWithJson(
requests, requests,
{ {
id: 1, id: 1,
name: defaultCohortName, name: defaultCohortName,
assignment_type: assignmentType,
group_id: contentGroupId, group_id: contentGroupId,
user_partition_id: MOCK_COHORTED_USER_PARTITION_ID user_partition_id: MOCK_COHORTED_USER_PARTITION_ID
} }
); );
AjaxHelpers.respondWithJson( AjaxHelpers.respondWithJson(
requests, requests,
{ cohorts: createMockCohort(defaultCohortName) } { cohorts: createMockCohort(defaultCohortName, 1, 0, null, null, assignmentType) }
); );
verifyMessage( verifyMessage(
'The ' + defaultCohortName + ' cohort has been created.' + 'The ' + defaultCohortName + ' cohort has been created.' +
' You can manually add students to this cohort below.', ' You can manually add students to this cohort below.',
'confirmation' 'confirmation'
); );
verifyHeader(1, defaultCohortName, 0); verifyHeader(1, defaultCohortName, 0, MOCK_RANDOM_ASSIGNMENT);
expect(cohortsView.$('.cohort-management-nav')).not.toHaveClass('is-disabled'); expect(cohortsView.$('.cohort-management-nav')).not.toHaveClass('is-disabled');
expect(cohortsView.$('.cohort-management-group')).not.toHaveClass('is-hidden'); expect(cohortsView.$('.cohort-management-group')).not.toHaveClass('is-hidden');
expect(getAddModal().find('.cohort-management-settings-form').length).toBe(0); expect(getAddModal().find('.cohort-management-settings-form').length).toBe(0);
}); });
it("has default assignment type set to manual", function() {
var cohortName = "iCohort";
createCohortsView(this, {cohorts: []});
cohortsView.$('.action-create').click();
cohortsView.$('.cohort-name').val(cohortName);
cohortsView.$('.action-save').click();
expectCohortAddRequest(cohortName, null, null, MOCK_MANUAL_ASSIGNMENT);
AjaxHelpers.respondWithJson(
requests,
{
id: 1,
name: cohortName,
assignment_type: MOCK_MANUAL_ASSIGNMENT,
group_id: null,
user_partition_id: null
}
);
AjaxHelpers.respondWithJson(
requests,
{ cohorts: createMockCohort(cohortName, 1, 0, null, null, MOCK_MANUAL_ASSIGNMENT) }
);
verifyHeader(1, cohortName, 0, MOCK_MANUAL_ASSIGNMENT);
});
it("trims off whitespace before adding a cohort", function() { it("trims off whitespace before adding a cohort", function() {
createCohortsView(this); createCohortsView(this);
cohortsView.$('.action-create').click(); cohortsView.$('.action-create').click();
cohortsView.$('.cohort-name').val(' New Cohort '); cohortsView.$('.cohort-name').val(' New Cohort ');
cohortsView.$('.action-save').click(); cohortsView.$('.action-save').click();
expectCohortAddRequest('New Cohort', null, null); expectCohortAddRequest('New Cohort', null, null, MOCK_MANUAL_ASSIGNMENT);
}); });
it("does not allow a blank cohort name to be submitted", function() { it("does not allow a blank cohort name to be submitted", function() {
...@@ -560,6 +646,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -560,6 +646,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
{ {
id: 1, id: 1,
name: 'Cat Lovers', name: 'Cat Lovers',
assignment_type: MOCK_RANDOM_ASSIGNMENT,
group_id: 999, group_id: 999,
user_partition_id: MOCK_COHORTED_USER_PARTITION_ID user_partition_id: MOCK_COHORTED_USER_PARTITION_ID
} }
...@@ -594,6 +681,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -594,6 +681,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
requests, 'PATCH', '/mock_service/cohorts/1', requests, 'PATCH', '/mock_service/cohorts/1',
{ {
name: 'Cat Lovers', name: 'Cat Lovers',
assignment_type: MOCK_MANUAL_ASSIGNMENT,
group_id: 0, group_id: 0,
user_partition_id: MOCK_COHORTED_USER_PARTITION_ID user_partition_id: MOCK_COHORTED_USER_PARTITION_ID
} }
...@@ -608,7 +696,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -608,7 +696,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
it("can clear selected content group", function () { it("can clear selected content group", function () {
createCohortsView(this, { createCohortsView(this, {
cohorts: [ cohorts: [
{id: 1, name: 'Cat Lovers', group_id: 0} {id: 1, name: 'Cat Lovers', group_id: 0, 'assignment_type': MOCK_MANUAL_ASSIGNMENT}
], ],
selectCohort: 1 selectCohort: 1
}); });
...@@ -622,6 +710,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -622,6 +710,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
requests, 'PATCH', '/mock_service/cohorts/1', requests, 'PATCH', '/mock_service/cohorts/1',
{ {
name: 'Cat Lovers', name: 'Cat Lovers',
'assignment_type': MOCK_MANUAL_ASSIGNMENT,
group_id: null, group_id: null,
user_partition_id: null user_partition_id: null
} }
...@@ -645,7 +734,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -645,7 +734,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
createCohortsViewWithDeletedContentGroup(this); createCohortsViewWithDeletedContentGroup(this);
cohortsView.$('.tab-settings a').click(); cohortsView.$('.tab-settings a').click();
expect(cohortsView.$('option.option-unavailable').text().trim()).toBe('Deleted Content Group'); expect(cohortsView.$('option.option-unavailable').text().trim()).toBe('Deleted Content Group');
expect(cohortsView.$('.copy-error').text().trim()).toBe( expect(cohortsView.$('.cohort-management-details-association-course .copy-error').text().trim()).toBe(
'Warning: The previously selected content group was deleted. Select another content group.' 'Warning: The previously selected content group was deleted. Select another content group.'
); );
}); });
...@@ -660,13 +749,13 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -660,13 +749,13 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
cohortsView.$('.action-save').click(); cohortsView.$('.action-save').click();
AjaxHelpers.respondWithJson( AjaxHelpers.respondWithJson(
requests, requests,
createMockCohort('Cat Lovers', 1, catLoversInitialCount, 0, 0) createMockCohort('Cat Lovers', 1, catLoversInitialCount, 0, 0, MOCK_RANDOM_ASSIGNMENT)
); );
verifyMessage('Saved cohort', 'confirmation'); verifyMessage('Saved cohort', 'confirmation');
// Verify that the deleted content group and associated message have been removed // Verify that the deleted content group and associated message have been removed
expect(cohortsView.$('option.option-unavailable').text().trim()).toBe(''); expect(cohortsView.$('option.option-unavailable').text().trim()).toBe('');
expect(cohortsView.$('.copy-error').text().trim()).toBe(''); expect(cohortsView.$('.cohort-management-details-association-course .copy-error').text().trim()).toBe('');
}); });
it("shows an error when saving with a deleted content group", function () { it("shows an error when saving with a deleted content group", function () {
...@@ -699,6 +788,96 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe ...@@ -699,6 +788,96 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
MOCK_STUDIO_GROUP_CONFIGURATIONS_URL MOCK_STUDIO_GROUP_CONFIGURATIONS_URL
); );
}); });
it("can update existing cohort settings", function () {
var cohortName = 'Transformers',
newCohortName = 'X Men';
var expectedRequest = function(assignment_type) {
return {
name: newCohortName,
assignment_type: assignment_type,
group_id: null,
user_partition_id: null
}
};
createCohortsView(this, {
cohorts: [
{
id: 1,
name: cohortName,
assignment_type: MOCK_RANDOM_ASSIGNMENT,
group_id: 111,
user_partition_id: MOCK_COHORTED_USER_PARTITION_ID
}
],
selectCohort: 1
});
// Select settings tab
cohortsView.$('.tab-settings a').click();
// Verify the existing cohort values
expect(cohortsView.$('.cohort-name').val()).toBe(cohortName);
expect(cohortsView.$('input[name="cohort-assignment-type"]:checked').val()).toBe(MOCK_RANDOM_ASSIGNMENT);
expect(cohortsView.$('.radio-yes').prop('checked')).toBeTruthy();
// Update existing cohort values
cohortsView.$('.cohort-name').val(newCohortName);
cohortsView.$('.type-manual').prop('checked', true).change();
clearContentGroup();
// Save the updated settings
cohortsView.$('.action-save').click();
AjaxHelpers.expectJsonRequest(
requests, 'PATCH', '/mock_service/cohorts/1',
expectedRequest(MOCK_MANUAL_ASSIGNMENT)
);
AjaxHelpers.respondWithJson(
requests,
createMockCohort(newCohortName, 1, 0, null, null)
);
// Verify the new/updated cohort values
expect(cohortsView.$('.cohort-name').val()).toBe(newCohortName);
expect(cohortsView.$('input[name="cohort-assignment-type"]:checked').val()).toBe(MOCK_MANUAL_ASSIGNMENT);
expect(cohortsView.$('.radio-no').prop('checked')).toBeTruthy();
verifyMessage('Saved cohort', 'confirmation');
// Now try to update existing cohort name with an empty name
// We can't save a cohort with empty name, so we should see an error message
cohortsView.$('.cohort-name').val('');
saveFormAndExpectErrors('update', ['You must specify a name for the cohort']);
});
it("assignment settings are disabled for default cohort", function() {
createCohortsView(this, {
cohorts: [
{
id: 1,
name: 'Cohort.me',
assignment_type: MOCK_RANDOM_ASSIGNMENT,
group_id: 111,
user_partition_id: MOCK_COHORTED_USER_PARTITION_ID
}
],
selectCohort: 1
});
// 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.");
});
it("cancel settings works", function() {
createCohortsView(this, {selectCohort: 1, contentGroups: []});
cohortsView.$('.tab-settings a').click();
cohortsView.$('.cohort-name').val('One Two Three');
cohortsView.$('.action-cancel').click();
expect(cohortsView.$('.tab-manage_students')).toHaveClass('is-selected');
expect(cohortsView.$('.tab-settings')).not.toHaveClass('is-selected');
});
}); });
}); });
}); });
......
...@@ -567,6 +567,12 @@ ...@@ -567,6 +567,12 @@
} }
} }
.cohort-management-assignment-type-settings {
&.is-disabled {
opacity: 0.50;
}
}
// cohort // cohort
.cohort-management-group-header { .cohort-management-group-header {
padding: $baseline; padding: $baseline;
......
<section class="cohort-management-settings has-tabs"> <section class="cohort-management-settings has-tabs">
<header class="cohort-management-group-header"> <header class="cohort-management-group-header"></header>
<h3 class="group-header-title" tabindex="-1">
<span class="title-value"><%- cohort.get('name') %></span>
<span class="group-count"><%-
interpolate(
ngettext('(contains %(student_count)s student)', '(contains %(student_count)s students)', cohort.get('user_count')),
{ student_count: cohort.get('user_count') },
true
)
%></span>
</h3>
<div class="cohort-management-group-setup">
<div class="setup-value">
<% if (cohort.get('assignment_type') == "none") { %>
<%- gettext("Students are added to this cohort only when you provide their email addresses or usernames on this page.") %>
<a href="http://edx.readthedocs.org/projects/edx-partner-course-staff/en/latest/cohorts/cohort_config.html#assign-students-to-cohort-groups-manually" class="incontext-help action-secondary action-help"><%= gettext("What does this mean?") %></a>
<% } else { %>
<%- gettext("Students are added to this cohort automatically.") %>
<a href="http://edx.readthedocs.org/projects/edx-partner-course-staff/en/latest/cohorts/cohorts_overview.html#all-automated-assignment" class="incontext-help action-secondary action-help"><%- gettext("What does this mean?") %></a>
<% } %>
</div>
<div class="setup-actions">
<% if (studioAdvancedSettingsUrl !== "None") { %>
<a href="<%= studioAdvancedSettingsUrl %>" class="action-secondary action-edit"><%- gettext("Edit settings in Studio") %></a>
<% } %>
</div>
</div>
</header>
<ul class="wrapper-tabs"> <ul class="wrapper-tabs">
<li class="tab tab-manage_students is-selected" data-tab="manage_students"><a href="#"><span class="sr"><%- gettext('Selected tab') %> </span><%- gettext("Manage Students") %></a></li> <li class="tab tab-manage_students is-selected" data-tab="manage_students"><a href="#"><span class="sr"><%- gettext('Selected tab') %> </span><%- gettext("Manage Students") %></a></li>
......
...@@ -6,23 +6,6 @@ ...@@ -6,23 +6,6 @@
<div class="tab-content is-visible new-cohort-form"> <div class="tab-content is-visible new-cohort-form">
<% } %> <% } %>
<div class="form-fields"> <div class="form-fields">
<%
// Don't allow renaming of existing cohorts yet as it doesn't interact well with
// the course's advanced setting for auto cohorting.
if (isNewCohort) {
%>
<div class="form-field">
<div class="cohort-management-settings-form-name field field-text">
<label for="cohort-name" class="form-label">
<%- gettext('Cohort Name') %> *
<span class="sr"><%- gettext('(Required Field)')%></span>
</label>
<input type="text" name="cohort-name" value="<%- cohort ? cohort.get('name') : '' %>" class="input cohort-name"
id="cohort-name"
placeholder="<%- gettext("Enter the name of the cohort") %>" required="required" />
</div>
</div>
<% } %>
<% <%
var foundSelected = false; var foundSelected = false;
...@@ -30,8 +13,44 @@ ...@@ -30,8 +13,44 @@
var selectedUserPartitionId = cohort.get('user_partition_id'); var selectedUserPartitionId = cohort.get('user_partition_id');
var hasSelectedContentGroup = selectedContentGroupId != null; var hasSelectedContentGroup = selectedContentGroupId != null;
var hasContentGroups = contentGroups.length > 0; var hasContentGroups = contentGroups.length > 0;
var assignment_type = cohort.get('assignment_type');
var cohort_name = cohort.get('name');
var cohort_name_value = isNewCohort ? '' : cohort_name;
var placeholder_value = isNewCohort ? gettext('Enter the name of the cohort') : '';
%> %>
<div class="form-field"> <div class="form-field">
<div class="cohort-management-settings-form-name field field-text">
<label for="cohort-name" class="form-label">
<%- gettext('Cohort Name') %> *
<span class="sr"><%- gettext('(Required Field)')%></span>
</label>
<input name="cohort-name" value="<%- cohort_name_value %>" class="input cohort-name" id="cohort-name"
placeholder="<%- placeholder_value %>" required="required" type="text">
</div>
<hr class="divider divider-lv1">
<% if (isDefaultCohort) { %>
<div class="cohort-management-assignment-type-settings field field-radio is-disabled" aria-disabled="true">
<% } else { %>
<div class="cohort-management-assignment-type-settings field field-radio">
<% } %>
<h4 class="form-label">
<%- 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") %>
</label>
<label>
<input type="radio" class="type-manual" name="cohort-assignment-type" value="manual" <%- assignment_type == 'manual' || isNewCohort ? 'checked="checked"' : '' %>/> <%- gettext("Manually Assigned") %>
</label>
</div>
<% 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.") %>
</p>
<% } %>
<hr class="divider divider-lv1">
<div class="cohort-management-details-association-course field field-radio"> <div class="cohort-management-details-association-course field field-radio">
<h4 class="form-label"> <h4 class="form-label">
<%- gettext('Associated Content Group') %> <%- gettext('Associated Content Group') %>
...@@ -119,12 +138,9 @@ ...@@ -119,12 +138,9 @@
<div class="form-actions <% if (isNewCohort) { %>new-cohort-form<% } %>"> <div class="form-actions <% if (isNewCohort) { %>new-cohort-form<% } %>">
<button class="form-submit button action-primary action-save"> <button class="form-submit button action-primary action-save">
<i class="icon fa fa-plus" aria-hidden="true"></i>
<%- gettext('Save') %> <%- gettext('Save') %>
</button> </button>
<% if (isNewCohort) { %> <a href="" class="form-cancel action-secondary action-cancel"><%- gettext('Cancel') %></a>
<a href="" class="form-cancel action-secondary action-cancel"><%- gettext('Cancel') %></a>
<% } %>
</div> </div>
</form> </form>
</div> </div>
<h3 class="group-header-title" tabindex="-1">
<span class="title-value"><%- cohort.get('name') %></span>
<span class="group-count"><%-
interpolate(
ngettext('(contains %(student_count)s student)', '(contains %(student_count)s students)', cohort.get('user_count')),
{ student_count: cohort.get('user_count') },
true
)
%></span>
</h3>
<div class="cohort-management-group-setup">
<div class="setup-value">
<% if (cohort.get('assignment_type') == "manual") { %>
<%- gettext("Students are added to this cohort only when you provide their email addresses or usernames on this page.") %>
<a href="http://edx.readthedocs.org/projects/edx-partner-course-staff/en/latest/cohorts/cohort_config.html#assign-students-to-cohort-groups-manually" class="incontext-help action-secondary action-help"><%= gettext("What does this mean?") %></a>
<% } else { %>
<%- gettext("Students are added to this cohort automatically.") %>
<a href="http://edx.readthedocs.org/projects/edx-partner-course-staff/en/latest/cohorts/cohorts_overview.html#all-automated-assignment" class="incontext-help action-secondary action-help"><%- gettext("What does this mean?") %></a>
<% } %>
</div>
<div class="setup-actions">
<% if (studioAdvancedSettingsUrl !== "None") { %>
<a href="<%= studioAdvancedSettingsUrl %>" class="action-secondary action-edit"><%- gettext("Edit settings in Studio") %></a>
<% } %>
</div>
</div>
\ No newline at end of file
...@@ -66,7 +66,7 @@ ...@@ -66,7 +66,7 @@
## Include Underscore templates ## Include Underscore templates
<%block name="header_extras"> <%block name="header_extras">
% for template_name in ["cohorts", "cohort-editor", "cohort-selector", "cohort-form", "notification"]: % for template_name in ["cohorts", "cohort-editor", "cohort-group-header", "cohort-selector", "cohort-form", "notification"]:
<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>
......
...@@ -5,6 +5,7 @@ forums, and to the cohort admin views. ...@@ -5,6 +5,7 @@ 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
...@@ -15,7 +16,7 @@ from django.utils.translation import ugettext as _ ...@@ -15,7 +16,7 @@ from django.utils.translation import ugettext as _
from courseware import courses from courseware import courses
from eventtracking import tracker from eventtracking import tracker
from student.models import get_user_by_username_or_email from student.models import get_user_by_username_or_email
from .models import CourseUserGroup, CourseUserGroupPartitionGroup from .models import CourseUserGroup, CourseCohort, CourseCohortsSettings, CourseUserGroupPartitionGroup
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -82,31 +83,6 @@ def _cohort_membership_changed(sender, **kwargs): ...@@ -82,31 +83,6 @@ def _cohort_membership_changed(sender, **kwargs):
DEFAULT_COHORT_NAME = "Default Group" DEFAULT_COHORT_NAME = "Default Group"
class CohortAssignmentType(object):
"""
The various types of rule-based cohorts
"""
# No automatic rules are applied to this cohort; users must be manually added.
NONE = "none"
# One of (possibly) multiple cohorts to which users are randomly assigned.
# Note: The 'default' cohort is included in this category iff it exists and
# there are no other random groups. (Also see Note 2 above.)
RANDOM = "random"
@staticmethod
def get(cohort, course):
"""
Returns the assignment type of the given cohort for the given course
"""
if cohort.name in course.auto_cohort_groups:
return CohortAssignmentType.RANDOM
elif len(course.auto_cohort_groups) == 0 and cohort.name == DEFAULT_COHORT_NAME:
return CohortAssignmentType.RANDOM
else:
return CohortAssignmentType.NONE
# tl;dr: global state is bad. capa reseeds random every time a problem is loaded. Even # tl;dr: global state is bad. capa reseeds random every time a problem is loaded. Even
# if and when that's fixed, it's a good idea to have a local generator to avoid any other # if and when that's fixed, it's a good idea to have a local generator to avoid any other
# code that messes with the global random module. # code that messes with the global random module.
...@@ -237,47 +213,74 @@ def get_cohort(user, course_key, assign=True): ...@@ -237,47 +213,74 @@ def get_cohort(user, course_key, assign=True):
if not assign: if not assign:
return None return None
choices = course.auto_cohort_groups cohorts = get_course_cohorts(course, assignment_type=CourseCohort.RANDOM)
if len(choices) > 0: if cohorts:
# Randomly choose one of the auto_cohort_groups, creating it if needed. cohort = local_random().choice(cohorts)
group_name = local_random().choice(choices)
else: else:
# Use the "default cohort". cohort = CourseCohort.create(
group_name = DEFAULT_COHORT_NAME cohort_name=DEFAULT_COHORT_NAME,
course_id=course_key,
assignment_type=CourseCohort.RANDOM
).course_user_group
group, __ = CourseUserGroup.objects.get_or_create( user.course_groups.add(cohort)
course_id=course_key,
group_type=CourseUserGroup.COHORT, return cohort
name=group_name
def migrate_cohort_settings(course):
"""
Migrate all the cohort settings associated with this course from modulestore to mysql.
After that we will never touch modulestore for any cohort related settings.
"""
course_id = course.location.course_key
cohort_settings, created = CourseCohortsSettings.objects.get_or_create(
course_id=course_id,
defaults={
'is_cohorted': course.is_cohorted,
'cohorted_discussions': json.dumps(list(course.cohorted_discussions)),
'always_cohort_inline_discussions': course.always_cohort_inline_discussions
}
) )
user.course_groups.add(group)
return group
# Add the new and update the existing cohorts
if created:
# Update the manual cohorts already present in CourseUserGroup
manual_cohorts = CourseUserGroup.objects.filter(
course_id=course_id,
group_type=CourseUserGroup.COHORT
).exclude(name__in=course.auto_cohort_groups)
for cohort in manual_cohorts:
CourseCohort.create(course_user_group=cohort)
for group_name in course.auto_cohort_groups:
CourseCohort.create(cohort_name=group_name, course_id=course_id, assignment_type=CourseCohort.RANDOM)
return cohort_settings
def get_course_cohorts(course):
def get_course_cohorts(course, assignment_type=None):
""" """
Get a list of all the cohorts in the given course. This will include auto cohorts, Get a list of all the cohorts in the given course. This will include auto cohorts,
regardless of whether or not the auto cohorts include any users. regardless of whether or not the auto cohorts include any users.
Arguments: Arguments:
course: the course for which cohorts should be returned course: the course for which cohorts should be returned
assignment_type: cohort assignment type
Returns: Returns:
A list of CourseUserGroup objects. Empty if there are no cohorts. Does A list of CourseUserGroup objects. Empty if there are no cohorts. Does
not check whether the course is cohorted. not check whether the course is cohorted.
""" """
# Ensure all auto cohorts are created. # Migrate cohort settings for this course
for group_name in course.auto_cohort_groups: migrate_cohort_settings(course)
CourseUserGroup.objects.get_or_create(
course_id=course.location.course_key,
group_type=CourseUserGroup.COHORT,
name=group_name
)
return list(CourseUserGroup.objects.filter( query_set = CourseUserGroup.objects.filter(
course_id=course.location.course_key, course_id=course.location.course_key,
group_type=CourseUserGroup.COHORT group_type=CourseUserGroup.COHORT
)) )
query_set = query_set.filter(cohort__assignment_type=assignment_type) if assignment_type else query_set
return list(query_set)
### Helpers for cohort management views ### Helpers for cohort management views
...@@ -297,7 +300,7 @@ def get_cohort_by_name(course_key, name): ...@@ -297,7 +300,7 @@ def get_cohort_by_name(course_key, name):
def get_cohort_by_id(course_key, cohort_id): def get_cohort_by_id(course_key, cohort_id):
""" """
Return the CourseUserGroup object for the given cohort. Raises DoesNotExist Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
it isn't present. Uses the course_key for extra validation... it isn't present. Uses the course_key for extra validation.
""" """
return CourseUserGroup.objects.get( return CourseUserGroup.objects.get(
course_id=course_key, course_id=course_key,
...@@ -306,15 +309,13 @@ def get_cohort_by_id(course_key, cohort_id): ...@@ -306,15 +309,13 @@ def get_cohort_by_id(course_key, cohort_id):
) )
def add_cohort(course_key, name): def add_cohort(course_key, name, assignment_type):
""" """
Add a cohort to a course. Raises ValueError if a cohort of the same name already Add a cohort to a course. Raises ValueError if a cohort of the same name already
exists. exists.
""" """
log.debug("Adding cohort %s to %s", name, course_key) log.debug("Adding cohort %s to %s", name, course_key)
if CourseUserGroup.objects.filter(course_id=course_key, if is_cohort_exists(course_key, name):
group_type=CourseUserGroup.COHORT,
name=name).exists():
raise ValueError(_("You cannot create two cohorts with the same name")) raise ValueError(_("You cannot create two cohorts with the same name"))
try: try:
...@@ -322,11 +323,11 @@ def add_cohort(course_key, name): ...@@ -322,11 +323,11 @@ def add_cohort(course_key, name):
except Http404: except Http404:
raise ValueError("Invalid course_key") raise ValueError("Invalid course_key")
cohort = CourseUserGroup.objects.create( cohort = CourseCohort.create(
course_id=course.id, cohort_name=name, course_id=course.id,
group_type=CourseUserGroup.COHORT, assignment_type=assignment_type
name=name ).course_user_group
)
tracker.emit( tracker.emit(
"edx.cohort.creation_requested", "edx.cohort.creation_requested",
{"cohort_name": cohort.name, "cohort_id": cohort.id} {"cohort_name": cohort.name, "cohort_id": cohort.id}
...@@ -334,6 +335,13 @@ def add_cohort(course_key, name): ...@@ -334,6 +335,13 @@ def add_cohort(course_key, name):
return cohort return cohort
def is_cohort_exists(course_key, name):
"""
Check if a cohort already exists.
"""
return CourseUserGroup.objects.filter(course_id=course_key, group_type=CourseUserGroup.COHORT, name=name).exists()
def add_user_to_cohort(cohort, username_or_email): def add_user_to_cohort(cohort, username_or_email):
""" """
Look up the given user, and if successful, add them to the specified cohort. Look up the given user, and if successful, add them to the specified cohort.
...@@ -396,3 +404,37 @@ def get_group_info_for_cohort(cohort): ...@@ -396,3 +404,37 @@ def get_group_info_for_cohort(cohort):
if len(res): if len(res):
return res[0].group_id, res[0].partition_id return res[0].group_id, res[0].partition_id
return None, None return None, None
def set_assignment_type(user_group, assignment_type):
"""
Set assignment type for cohort.
"""
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."))
course_cohort.assignment_type = assignment_type
course_cohort.save()
def get_assignment_type(user_group):
"""
Get assignment type for cohort.
"""
course_cohort = user_group.cohort
return course_cohort.assignment_type
def is_default_cohort(user_group):
"""
Check if a cohort is default.
"""
random_cohorts = CourseUserGroup.objects.filter(
course_id=user_group.course_id,
group_type=CourseUserGroup.COHORT,
cohort__assignment_type=CourseCohort.RANDOM
)
return len(random_cohorts) == 1 and random_cohorts[0].name == user_group.name
# -*- 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):
# Adding model 'CourseCohort'
db.create_table('course_groups_coursecohort', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_user_group', self.gf('django.db.models.fields.related.OneToOneField')(related_name='cohort', unique=True, to=orm['course_groups.CourseUserGroup'])),
('assignment_type', self.gf('django.db.models.fields.CharField')(default='manual', max_length=20)),
))
db.send_create_signal('course_groups', ['CourseCohort'])
# Adding model 'CourseCohortsSettings'
db.create_table('course_groups_coursecohortssettings', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('is_cohorted', self.gf('django.db.models.fields.BooleanField')(default=False)),
('course_id', self.gf('xmodule_django.models.CourseKeyField')(unique=True, max_length=255, db_index=True)),
('cohorted_discussions', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
('always_cohort_inline_discussions', self.gf('django.db.models.fields.BooleanField')(default=True)),
))
db.send_create_signal('course_groups', ['CourseCohortsSettings'])
def backwards(self, orm):
# Deleting model 'CourseCohort'
db.delete_table('course_groups_coursecohort')
# Deleting model 'CourseCohortsSettings'
db.delete_table('course_groups_coursecohortssettings')
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'},
'always_cohort_inline_discussions': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'cohorted_discussions': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': '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
...@@ -36,9 +36,26 @@ class CourseUserGroup(models.Model): ...@@ -36,9 +36,26 @@ class CourseUserGroup(models.Model):
GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'),) GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'),)
group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES) group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES)
@classmethod
def create(cls, name, course_id, group_type=COHORT):
"""
Create a new course user group.
Args:
name: Name of group
course_id: course id
group_type: group type
"""
return cls.objects.get_or_create(
course_id=course_id,
group_type=group_type,
name=name
)
class CourseUserGroupPartitionGroup(models.Model): class CourseUserGroupPartitionGroup(models.Model):
""" """
Create User Partition Info.
""" """
course_user_group = models.OneToOneField(CourseUserGroup) course_user_group = models.OneToOneField(CourseUserGroup)
partition_id = models.IntegerField( partition_id = models.IntegerField(
...@@ -49,3 +66,55 @@ class CourseUserGroupPartitionGroup(models.Model): ...@@ -49,3 +66,55 @@ class CourseUserGroupPartitionGroup(models.Model):
) )
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
class CourseCohortsSettings(models.Model):
"""
This model represents cohort settings for courses.
"""
is_cohorted = models.BooleanField(default=False)
course_id = CourseKeyField(
unique=True,
max_length=255,
db_index=True,
help_text="Which course are these settings associated with?",
)
cohorted_discussions = models.TextField(null=True, blank=True) # JSON list
# pylint: disable=invalid-name
always_cohort_inline_discussions = models.BooleanField(default=True)
class CourseCohort(models.Model):
"""
This model represents cohort related info.
"""
course_user_group = models.OneToOneField(CourseUserGroup, unique=True, related_name='cohort')
RANDOM = 'random'
MANUAL = 'manual'
ASSIGNMENT_TYPE_CHOICES = ((RANDOM, 'Random'), (MANUAL, 'Manual'),)
assignment_type = models.CharField(max_length=20, choices=ASSIGNMENT_TYPE_CHOICES, default=MANUAL)
@classmethod
def create(cls, cohort_name=None, course_id=None, course_user_group=None, assignment_type=MANUAL):
"""
Create a complete(CourseUserGroup + CourseCohort) object.
Args:
cohort_name: Name of the cohort to be created
course_id: Course Id
course_user_group: CourseUserGroup
assignment_type: 'random' or 'manual'
"""
if course_user_group is None:
course_user_group, __ = CourseUserGroup.create(cohort_name, course_id)
course_cohort, __ = cls.objects.get_or_create(
course_user_group=course_user_group,
defaults={'assignment_type': assignment_type}
)
return course_cohort
""" """
Helper methods for testing cohorts. Helper methods for testing cohorts.
""" """
import factory
from factory import post_generation, Sequence from factory import post_generation, Sequence
from factory.django import DjangoModelFactory from factory.django import DjangoModelFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey 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 from ..models import CourseUserGroup, CourseCohort
class CohortFactory(DjangoModelFactory): class CohortFactory(DjangoModelFactory):
...@@ -29,6 +30,16 @@ class CohortFactory(DjangoModelFactory): ...@@ -29,6 +30,16 @@ class CohortFactory(DjangoModelFactory):
self.users.add(*extracted) self.users.add(*extracted)
class CourseCohortFactory(DjangoModelFactory):
"""
Factory for constructing mock course cohort.
"""
FACTORY_FOR = CourseCohort
course_user_group = factory.SubFactory(CohortFactory)
assignment_type = 'manual'
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
......
"""
Tests for cohorts
"""
# pylint: disable=no-member
from django.conf import settings 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
...@@ -12,9 +17,9 @@ from student.tests.factories import UserFactory ...@@ -12,9 +17,9 @@ from student.tests.factories import UserFactory
from xmodule.modulestore.django import modulestore, clear_existing_modulestores from xmodule.modulestore.django import modulestore, clear_existing_modulestores
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTORE, mixed_store_config, ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTORE, mixed_store_config, ModuleStoreTestCase
from ..models import CourseUserGroup, 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 from ..tests.helpers import topic_name_to_id, config_course_cohorts, CohortFactory, CourseCohortFactory
@patch("openedx.core.djangoapps.course_groups.cohorts.tracker") @patch("openedx.core.djangoapps.course_groups.cohorts.tracker")
...@@ -128,6 +133,14 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -128,6 +133,14 @@ class TestCohorts(ModuleStoreTestCase):
super(TestCohorts, self).setUp() super(TestCohorts, self).setUp()
self.toy_course_key = SlashSeparatedCourseKey("edX", "toy", "2012_Fall") self.toy_course_key = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
def _create_cohort(self, course_id, cohort_name, assignment_type):
"""
Create a cohort for testing.
"""
cohort = CohortFactory(course_id=course_id, name=cohort_name)
CourseCohortFactory(course_user_group=cohort, assignment_type=assignment_type)
return cohort
def test_is_course_cohorted(self): def test_is_course_cohorted(self):
""" """
Make sure cohorts.is_course_cohorted() correctly reports if a course is cohorted or not. Make sure cohorts.is_course_cohorted() correctly reports if a course is cohorted or not.
...@@ -166,6 +179,42 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -166,6 +179,42 @@ class TestCohorts(ModuleStoreTestCase):
lambda: cohorts.get_cohort_id(user, SlashSeparatedCourseKey("course", "does_not", "exist")) lambda: cohorts.get_cohort_id(user, SlashSeparatedCourseKey("course", "does_not", "exist"))
) )
def test_assignment_type(self):
"""
Make sure that cohorts.set_assignment_type() and cohorts.get_assignment_type() works correctly.
"""
course = modulestore().get_course(self.toy_course_key)
# We are creating two random cohorts because we can't change assignment type of
# random cohort if it is the only random cohort present.
cohort1 = self._create_cohort(course.id, "TestCohort1", CourseCohort.RANDOM)
self._create_cohort(course.id, "TestCohort2", CourseCohort.RANDOM)
cohort3 = self._create_cohort(course.id, "TestCohort3", CourseCohort.MANUAL)
self.assertEqual(cohorts.get_assignment_type(cohort1), CourseCohort.RANDOM)
cohorts.set_assignment_type(cohort1, CourseCohort.MANUAL)
self.assertEqual(cohorts.get_assignment_type(cohort1), CourseCohort.MANUAL)
cohorts.set_assignment_type(cohort3, CourseCohort.RANDOM)
self.assertEqual(cohorts.get_assignment_type(cohort3), CourseCohort.RANDOM)
def test_cannot_set_assignment_type(self):
"""
Make sure that we can't change the assignment type of a random cohort if it is the only random cohort present.
"""
course = modulestore().get_course(self.toy_course_key)
cohort = self._create_cohort(course.id, "TestCohort", 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."
with self.assertRaises(ValueError) as context_manager:
cohorts.set_assignment_type(cohort, CourseCohort.MANUAL)
self.assertEqual(exception_msg, str(context_manager.exception))
def test_get_cohort(self): def test_get_cohort(self):
""" """
Make sure cohorts.get_cohort() does the right thing when the course is cohorted Make sure cohorts.get_cohort() does the right thing when the course is cohorted
...@@ -225,17 +274,16 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -225,17 +274,16 @@ class TestCohorts(ModuleStoreTestCase):
# get_cohort should return a group for user # get_cohort should return a group for user
self.assertEquals(cohorts.get_cohort(user, course.id).name, "AutoGroup") self.assertEquals(cohorts.get_cohort(user, course.id).name, "AutoGroup")
def test_auto_cohorting(self): def test_cohorting_with_auto_cohort_groups(self):
""" """
Make sure cohorts.get_cohort() does the right thing with auto_cohort_groups Make sure cohorts.get_cohort() does the right thing with auto_cohort_groups.
If there are auto cohort groups then a user should be assigned one.
""" """
course = modulestore().get_course(self.toy_course_key) course = modulestore().get_course(self.toy_course_key)
self.assertFalse(course.is_cohorted) self.assertFalse(course.is_cohorted)
user1 = UserFactory(username="test", email="a@b.com") user1 = UserFactory(username="test", email="a@b.com")
user2 = UserFactory(username="test2", email="a2@b.com") user2 = UserFactory(username="test2", email="a2@b.com")
user3 = UserFactory(username="test3", email="a3@b.com")
user4 = UserFactory(username="test4", email="a4@b.com")
cohort = CohortFactory(course_id=course.id, name="TestCohort") cohort = CohortFactory(course_id=course.id, name="TestCohort")
...@@ -254,21 +302,28 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -254,21 +302,28 @@ class TestCohorts(ModuleStoreTestCase):
self.assertEquals(cohorts.get_cohort(user2, course.id).name, "AutoGroup", "user2 should be auto-cohorted") self.assertEquals(cohorts.get_cohort(user2, course.id).name, "AutoGroup", "user2 should be auto-cohorted")
# Now make the auto_cohort_group list empty def test_cohorting_with_migrations_done(self):
"""
Verifies that cohort config changes on studio/moduletore side will
not be reflected on lms after the migrations are done.
"""
course = modulestore().get_course(self.toy_course_key)
user1 = UserFactory(username="test", email="a@b.com")
user2 = UserFactory(username="test2", email="a2@b.com")
# Add an auto_cohort_group to the course...
config_course_cohorts( config_course_cohorts(
course, course,
discussions=[], discussions=[],
cohorted=True, cohorted=True,
auto_cohort_groups=[] auto_cohort_groups=["AutoGroup"]
) )
self.assertEquals( self.assertEquals(cohorts.get_cohort(user1, course.id).name, "AutoGroup", "user1 should be auto-cohorted")
cohorts.get_cohort(user3, course.id).id,
cohorts.get_cohort_by_name(course.id, cohorts.DEFAULT_COHORT_NAME).id,
"No groups->default cohort"
)
# Now set the auto_cohort_group to something different # Now set the auto_cohort_group to something different
# This will have no effect on lms side as we are already done with migrations
config_course_cohorts( config_course_cohorts(
course, course,
discussions=[], discussions=[],
...@@ -277,20 +332,62 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -277,20 +332,62 @@ class TestCohorts(ModuleStoreTestCase):
) )
self.assertEquals( self.assertEquals(
cohorts.get_cohort(user4, course.id).name, "OtherGroup", "New list->new group" cohorts.get_cohort(user2, course.id).name, "AutoGroup", "user2 should be assigned to AutoGroups"
) )
self.assertEquals( self.assertEquals(
cohorts.get_cohort(user1, course.id).name, "TestCohort", "user1 should still be in originally placed cohort" cohorts.get_cohort(user1, course.id).name, "AutoGroup", "user1 should still be in originally placed cohort"
) )
def test_cohorting_with_no_auto_cohort_groups(self):
"""
Make sure cohorts.get_cohort() does the right thing with auto_cohort_groups.
If there are not auto cohort groups then a user should be assigned to Default Cohort Group.
Also verifies that cohort config changes on studio/moduletore side will
not be reflected on lms after the migrations are done.
"""
course = modulestore().get_course(self.toy_course_key)
self.assertFalse(course.is_cohorted)
user1 = UserFactory(username="test", email="a@b.com")
user2 = UserFactory(username="test2", email="a2@b.com")
# Make the auto_cohort_group list empty
config_course_cohorts(
course,
discussions=[],
cohorted=True,
auto_cohort_groups=[]
)
self.assertEquals( self.assertEquals(
cohorts.get_cohort(user2, course.id).name, "AutoGroup", "user2 should still be in originally placed cohort" cohorts.get_cohort(user1, course.id).id,
cohorts.get_cohort_by_name(course.id, cohorts.DEFAULT_COHORT_NAME).id,
"No groups->default cohort for user1"
)
# Add an auto_cohort_group to the course
# This will have no effect on lms side as we are already done with migrations
config_course_cohorts(
course,
discussions=[],
cohorted=True,
auto_cohort_groups=["AutoGroup"]
) )
self.assertEquals( self.assertEquals(
cohorts.get_cohort(user3, course.id).name, cohorts.get_cohort(user1, course.id).name,
cohorts.get_cohort_by_name(course.id, cohorts.DEFAULT_COHORT_NAME).name, cohorts.get_cohort_by_name(course.id, cohorts.DEFAULT_COHORT_NAME).name,
"user3 should still be in the default cohort" "user1 should still be in the default cohort"
)
self.assertEquals(
cohorts.get_cohort(user2, course.id).id,
cohorts.get_cohort_by_name(course.id, cohorts.DEFAULT_COHORT_NAME).id,
"No groups->default cohort for user2"
) )
def test_auto_cohorting_randomization(self): def test_auto_cohorting_randomization(self):
""" """
Make sure cohorts.get_cohort() randomizes properly. Make sure cohorts.get_cohort() randomizes properly.
...@@ -512,8 +609,9 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -512,8 +609,9 @@ class TestCohorts(ModuleStoreTestCase):
Make sure cohorts.add_cohort() properly adds a cohort to a course and handles Make sure cohorts.add_cohort() properly adds a cohort to a course and handles
errors. errors.
""" """
assignment_type = CourseCohort.RANDOM
course = modulestore().get_course(self.toy_course_key) course = modulestore().get_course(self.toy_course_key)
added_cohort = cohorts.add_cohort(course.id, "My Cohort") added_cohort = cohorts.add_cohort(course.id, "My Cohort", assignment_type)
mock_tracker.emit.assert_any_call( mock_tracker.emit.assert_any_call(
"edx.cohort.creation_requested", "edx.cohort.creation_requested",
{"cohort_name": added_cohort.name, "cohort_id": added_cohort.id} {"cohort_name": added_cohort.name, "cohort_id": added_cohort.id}
...@@ -522,11 +620,12 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -522,11 +620,12 @@ class TestCohorts(ModuleStoreTestCase):
self.assertEqual(added_cohort.name, "My Cohort") self.assertEqual(added_cohort.name, "My Cohort")
self.assertRaises( self.assertRaises(
ValueError, ValueError,
lambda: cohorts.add_cohort(course.id, "My Cohort") lambda: cohorts.add_cohort(course.id, "My Cohort", assignment_type)
) )
does_not_exist_course_key = SlashSeparatedCourseKey("course", "does_not", "exist")
self.assertRaises( self.assertRaises(
ValueError, ValueError,
lambda: cohorts.add_cohort(SlashSeparatedCourseKey("course", "does_not", "exist"), "My Cohort") lambda: cohorts.add_cohort(does_not_exist_course_key, "My Cohort", assignment_type)
) )
@patch("openedx.core.djangoapps.course_groups.cohorts.tracker") @patch("openedx.core.djangoapps.course_groups.cohorts.tracker")
...@@ -589,6 +688,9 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -589,6 +688,9 @@ class TestCohorts(ModuleStoreTestCase):
class TestCohortsAndPartitionGroups(ModuleStoreTestCase): class TestCohortsAndPartitionGroups(ModuleStoreTestCase):
MODULESTORE = TEST_DATA_MIXED_TOY_MODULESTORE MODULESTORE = TEST_DATA_MIXED_TOY_MODULESTORE
"""
Test Cohorts and Partitions Groups.
"""
def setUp(self): def setUp(self):
""" """
......
...@@ -76,6 +76,7 @@ class TestCohortPartitionScheme(ModuleStoreTestCase): ...@@ -76,6 +76,7 @@ class TestCohortPartitionScheme(ModuleStoreTestCase):
first_cohort, second_cohort = [ first_cohort, second_cohort = [
CohortFactory(course_id=self.course_key) for _ in range(2) CohortFactory(course_id=self.course_key) for _ in range(2)
] ]
# place student 0 into first cohort # place student 0 into first cohort
add_user_to_cohort(first_cohort, self.student.username) add_user_to_cohort(first_cohort, self.student.username)
self.assert_student_in_group(None) self.assert_student_in_group(None)
......
""" """
Tests for course group views Tests for course group views
""" """
# pylint: disable=attribute-defined-outside-init
# pylint: disable=no-member
from collections import namedtuple from collections import namedtuple
import json import json
...@@ -16,15 +18,15 @@ from xmodule.modulestore.tests.factories import CourseFactory ...@@ -16,15 +18,15 @@ from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from ..models import CourseUserGroup from ..models import CourseUserGroup, CourseCohort
from ..views import ( from ..views import (
cohort_handler, users_in_cohort, add_users_to_cohort, remove_user_from_cohort, link_cohort_to_partition_group cohort_handler, users_in_cohort, add_users_to_cohort, remove_user_from_cohort, link_cohort_to_partition_group
) )
from ..cohorts import ( from ..cohorts import (
get_cohort, CohortAssignmentType, get_cohort_by_name, get_cohort_by_id, get_cohort, get_cohort_by_name, get_cohort_by_id,
DEFAULT_COHORT_NAME, get_group_info_for_cohort DEFAULT_COHORT_NAME, get_group_info_for_cohort
) )
from .helpers import config_course_cohorts, CohortFactory from .helpers import config_course_cohorts, CohortFactory, CourseCohortFactory
class CohortViewsTestCase(ModuleStoreTestCase): class CohortViewsTestCase(ModuleStoreTestCase):
...@@ -47,15 +49,22 @@ class CohortViewsTestCase(ModuleStoreTestCase): ...@@ -47,15 +49,22 @@ class CohortViewsTestCase(ModuleStoreTestCase):
self.cohort1_users = [UserFactory() for _ in range(3)] self.cohort1_users = [UserFactory() for _ in range(3)]
self.cohort2_users = [UserFactory() for _ in range(2)] self.cohort2_users = [UserFactory() for _ in range(2)]
self.cohort3_users = [UserFactory() for _ in range(2)] self.cohort3_users = [UserFactory() for _ in range(2)]
self.cohort4_users = [UserFactory() for _ in range(2)]
self.cohortless_users = [UserFactory() for _ in range(3)] self.cohortless_users = [UserFactory() for _ in range(3)]
self.unenrolled_users = [UserFactory() for _ in range(3)] self.unenrolled_users = [UserFactory() for _ in range(3)]
self._enroll_users( self._enroll_users(
self.cohort1_users + self.cohort2_users + self.cohort3_users + self.cohortless_users, self.cohort1_users + self.cohort2_users + self.cohort3_users + self.cohortless_users + self.cohort4_users,
self.course.id self.course.id
) )
self.cohort1 = CohortFactory(course_id=self.course.id, users=self.cohort1_users) self.cohort1 = CohortFactory(course_id=self.course.id, users=self.cohort1_users)
self.cohort2 = CohortFactory(course_id=self.course.id, users=self.cohort2_users) self.cohort2 = CohortFactory(course_id=self.course.id, users=self.cohort2_users)
self.cohort3 = CohortFactory(course_id=self.course.id, users=self.cohort3_users) self.cohort3 = CohortFactory(course_id=self.course.id, users=self.cohort3_users)
self.cohort4 = CohortFactory(course_id=self.course.id, users=self.cohort4_users)
CourseCohortFactory(course_user_group=self.cohort1)
CourseCohortFactory(course_user_group=self.cohort2)
CourseCohortFactory(course_user_group=self.cohort3)
CourseCohortFactory(course_user_group=self.cohort4, assignment_type=CourseCohort.RANDOM)
def _user_in_cohort(self, username, cohort): def _user_in_cohort(self, username, cohort):
""" """
...@@ -171,9 +180,10 @@ class CohortHandlerTestCase(CohortViewsTestCase): ...@@ -171,9 +180,10 @@ class CohortHandlerTestCase(CohortViewsTestCase):
""" """
self._create_cohorts() self._create_cohorts()
expected_cohorts = [ expected_cohorts = [
CohortHandlerTestCase.create_expected_cohort(self.cohort1, 3, CohortAssignmentType.NONE), CohortHandlerTestCase.create_expected_cohort(self.cohort1, 3, CourseCohort.MANUAL),
CohortHandlerTestCase.create_expected_cohort(self.cohort2, 2, CohortAssignmentType.NONE), CohortHandlerTestCase.create_expected_cohort(self.cohort2, 2, CourseCohort.MANUAL),
CohortHandlerTestCase.create_expected_cohort(self.cohort3, 2, CohortAssignmentType.NONE), CohortHandlerTestCase.create_expected_cohort(self.cohort3, 2, CourseCohort.MANUAL),
CohortHandlerTestCase.create_expected_cohort(self.cohort4, 2, CourseCohort.RANDOM),
] ]
self.verify_lists_expected_cohorts(expected_cohorts) self.verify_lists_expected_cohorts(expected_cohorts)
...@@ -192,11 +202,12 @@ class CohortHandlerTestCase(CohortViewsTestCase): ...@@ -192,11 +202,12 @@ class CohortHandlerTestCase(CohortViewsTestCase):
auto_cohort_1 = get_cohort_by_name(self.course.id, "AutoGroup1") auto_cohort_1 = get_cohort_by_name(self.course.id, "AutoGroup1")
auto_cohort_2 = get_cohort_by_name(self.course.id, "AutoGroup2") auto_cohort_2 = get_cohort_by_name(self.course.id, "AutoGroup2")
expected_cohorts = [ expected_cohorts = [
CohortHandlerTestCase.create_expected_cohort(self.cohort1, 3, CohortAssignmentType.NONE), CohortHandlerTestCase.create_expected_cohort(self.cohort1, 3, CourseCohort.MANUAL),
CohortHandlerTestCase.create_expected_cohort(self.cohort2, 2, CohortAssignmentType.NONE), CohortHandlerTestCase.create_expected_cohort(self.cohort2, 2, CourseCohort.MANUAL),
CohortHandlerTestCase.create_expected_cohort(self.cohort3, 2, CohortAssignmentType.NONE), CohortHandlerTestCase.create_expected_cohort(self.cohort3, 2, CourseCohort.MANUAL),
CohortHandlerTestCase.create_expected_cohort(auto_cohort_1, 0, CohortAssignmentType.RANDOM), CohortHandlerTestCase.create_expected_cohort(self.cohort4, 2, CourseCohort.RANDOM),
CohortHandlerTestCase.create_expected_cohort(auto_cohort_2, 0, CohortAssignmentType.RANDOM), CohortHandlerTestCase.create_expected_cohort(auto_cohort_1, 0, CourseCohort.RANDOM),
CohortHandlerTestCase.create_expected_cohort(auto_cohort_2, 0, CourseCohort.RANDOM),
] ]
self.verify_lists_expected_cohorts(expected_cohorts, actual_cohorts) self.verify_lists_expected_cohorts(expected_cohorts, actual_cohorts)
...@@ -218,6 +229,7 @@ class CohortHandlerTestCase(CohortViewsTestCase): ...@@ -218,6 +229,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
self._enroll_users(users, self.course.id) self._enroll_users(users, self.course.id)
# mimic users accessing the discussion forum # mimic users accessing the discussion forum
# Default Cohort will be created here
for user in users: for user in users:
get_cohort(user, self.course.id) get_cohort(user, self.course.id)
...@@ -225,21 +237,18 @@ class CohortHandlerTestCase(CohortViewsTestCase): ...@@ -225,21 +237,18 @@ class CohortHandlerTestCase(CohortViewsTestCase):
default_cohort = get_cohort_by_name(self.course.id, DEFAULT_COHORT_NAME) default_cohort = get_cohort_by_name(self.course.id, DEFAULT_COHORT_NAME)
actual_cohorts = self.get_cohort_handler(self.course) actual_cohorts = self.get_cohort_handler(self.course)
self.verify_lists_expected_cohorts( self.verify_lists_expected_cohorts(
[CohortHandlerTestCase.create_expected_cohort(default_cohort, len(users), CohortAssignmentType.RANDOM)], [CohortHandlerTestCase.create_expected_cohort(default_cohort, len(users), CourseCohort.RANDOM)],
actual_cohorts, actual_cohorts,
) )
# set auto_cohort_groups and verify the default cohort is no longer listed as RANDOM # set auto_cohort_groups
# these cohort config will have not effect on lms side as we are already done with migrations
config_course_cohorts(self.course, [], cohorted=True, auto_cohort_groups=["AutoGroup"]) config_course_cohorts(self.course, [], cohorted=True, auto_cohort_groups=["AutoGroup"])
actual_cohorts = self.get_cohort_handler(self.course)
auto_cohort = get_cohort_by_name(self.course.id, "AutoGroup") # We should expect the DoesNotExist exception because above cohort config have
self.verify_lists_expected_cohorts( # no effect on lms side so as a result there will be no AutoGroup cohort present
[ with self.assertRaises(CourseUserGroup.DoesNotExist):
CohortHandlerTestCase.create_expected_cohort(default_cohort, len(users), CohortAssignmentType.NONE), get_cohort_by_name(self.course.id, "AutoGroup")
CohortHandlerTestCase.create_expected_cohort(auto_cohort, 0, CohortAssignmentType.RANDOM),
],
actual_cohorts,
)
def test_get_single_cohort(self): def test_get_single_cohort(self):
""" """
...@@ -253,7 +262,7 @@ class CohortHandlerTestCase(CohortViewsTestCase): ...@@ -253,7 +262,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
"name": self.cohort2.name, "name": self.cohort2.name,
"id": self.cohort2.id, "id": self.cohort2.id,
"user_count": 2, "user_count": 2,
"assignment_type": "none", "assignment_type": CourseCohort.MANUAL,
"user_partition_id": None, "user_partition_id": None,
"group_id": None "group_id": None
} }
...@@ -262,7 +271,8 @@ class CohortHandlerTestCase(CohortViewsTestCase): ...@@ -262,7 +271,8 @@ class CohortHandlerTestCase(CohortViewsTestCase):
############### Tests of adding a new cohort ############### ############### Tests of adding a new cohort ###############
def verify_contains_added_cohort( def verify_contains_added_cohort(
self, response_dict, cohort_name, expected_user_partition_id=None, expected_group_id=None self, response_dict, cohort_name, assignment_type=CourseCohort.MANUAL,
expected_user_partition_id=None, expected_group_id=None
): ):
""" """
Verifies that the cohort was created properly and the correct response was returned. Verifies that the cohort was created properly and the correct response was returned.
...@@ -275,7 +285,7 @@ class CohortHandlerTestCase(CohortViewsTestCase): ...@@ -275,7 +285,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
"name": cohort_name, "name": cohort_name,
"id": created_cohort.id, "id": created_cohort.id,
"user_count": 0, "user_count": 0,
"assignment_type": CohortAssignmentType.NONE, "assignment_type": assignment_type,
"user_partition_id": expected_user_partition_id, "user_partition_id": expected_user_partition_id,
"group_id": expected_group_id "group_id": expected_group_id
} }
...@@ -287,21 +297,38 @@ class CohortHandlerTestCase(CohortViewsTestCase): ...@@ -287,21 +297,38 @@ class CohortHandlerTestCase(CohortViewsTestCase):
Verify that a new cohort can be created, with and without user_partition_id/group_id information. Verify that a new cohort can be created, with and without user_partition_id/group_id information.
""" """
new_cohort_name = "New cohort unassociated to content groups" new_cohort_name = "New cohort unassociated to content groups"
response_dict = self.put_cohort_handler(self.course, data={'name': new_cohort_name}) request_data = {'name': new_cohort_name, 'assignment_type': CourseCohort.RANDOM}
self.verify_contains_added_cohort(response_dict, new_cohort_name) response_dict = self.put_cohort_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" new_cohort_name = "New cohort linked to group"
response_dict = self.put_cohort_handler( data = {
self.course, data={'name': new_cohort_name, 'user_partition_id': 1, 'group_id': 2} 'name': new_cohort_name,
'assignment_type': CourseCohort.MANUAL,
'user_partition_id': 1,
'group_id': 2
}
response_dict = self.put_cohort_handler(self.course, data=data)
self.verify_contains_added_cohort(
response_dict,
new_cohort_name,
expected_user_partition_id=1,
expected_group_id=2
) )
self.verify_contains_added_cohort(response_dict, new_cohort_name, 1, 2)
def test_create_new_cohort_missing_name(self): def test_create_new_cohort_missing_name(self):
""" """
Verify that we cannot create a cohort without specifying a name. 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_cohort_handler(self.course, expected_response_code=400)
self.assertEqual("In order to create a cohort, a name must be specified.", response_dict.get("error")) 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)
self.assertEqual("Assignment type must be specified.", response_dict.get("error"))
def test_create_new_cohort_existing_name(self): def test_create_new_cohort_existing_name(self):
""" """
...@@ -309,7 +336,8 @@ class CohortHandlerTestCase(CohortViewsTestCase): ...@@ -309,7 +336,8 @@ class CohortHandlerTestCase(CohortViewsTestCase):
""" """
self._create_cohorts() self._create_cohorts()
response_dict = self.put_cohort_handler( response_dict = self.put_cohort_handler(
self.course, data={'name': self.cohort1.name}, expected_response_code=400 self.course, data={'name': self.cohort1.name, 'assignment_type': CourseCohort.MANUAL},
expected_response_code=400
) )
self.assertEqual("You cannot create two cohorts with the same name", response_dict.get("error")) self.assertEqual("You cannot create two cohorts with the same name", response_dict.get("error"))
...@@ -317,9 +345,8 @@ class CohortHandlerTestCase(CohortViewsTestCase): ...@@ -317,9 +345,8 @@ class CohortHandlerTestCase(CohortViewsTestCase):
""" """
Verify that we cannot create a cohort with a group_id if the user_partition_id is not also specified. Verify that we cannot create a cohort with a group_id if the user_partition_id is not also specified.
""" """
response_dict = self.put_cohort_handler( data = {'name': "Cohort missing user_partition_id", 'assignment_type': CourseCohort.MANUAL, 'group_id': 2}
self.course, data={'name': "Cohort missing user_partition_id", 'group_id': 2}, expected_response_code=400 response_dict = self.put_cohort_handler(self.course, data=data, expected_response_code=400)
)
self.assertEqual( self.assertEqual(
"If group_id is specified, user_partition_id must also be specified.", response_dict.get("error") "If group_id is specified, user_partition_id must also be specified.", response_dict.get("error")
) )
...@@ -332,32 +359,53 @@ class CohortHandlerTestCase(CohortViewsTestCase): ...@@ -332,32 +359,53 @@ class CohortHandlerTestCase(CohortViewsTestCase):
""" """
self._create_cohorts() self._create_cohorts()
updated_name = self.cohort1.name + "_updated" updated_name = self.cohort1.name + "_updated"
response_dict = self.put_cohort_handler(self.course, self.cohort1, {'name': updated_name}) data = {'name': updated_name, 'assignment_type': CourseCohort.MANUAL}
response_dict = self.put_cohort_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, get_cohort_by_id(self.course.id, self.cohort1.id).name)
self.assertEqual(updated_name, response_dict.get("name")) self.assertEqual(updated_name, response_dict.get("name"))
self.assertEqual(CohortAssignmentType.NONE, response_dict.get("assignment_type")) self.assertEqual(CourseCohort.MANUAL, response_dict.get("assignment_type"))
self.assertEqual(CohortAssignmentType.NONE, CohortAssignmentType.get(self.cohort1, self.course))
def test_update_random_cohort_name_not_supported(self): def test_update_random_cohort_name(self):
""" """
Test that it is not possible to update the name of an existing random cohort. Test that it is possible to update the name of an existing random cohort.
""" """
random_cohort = CohortFactory(course_id=self.course.id) # Create a new cohort with random assignment
random_cohort_name = random_cohort.name 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)
# Update course cohort_config so random_cohort is in the list of auto cohorts. self.assertEqual(cohort_name, response_dict.get("name"))
self.course.cohort_config["auto_cohort_groups"] = [random_cohort_name] self.assertEqual(CourseCohort.RANDOM, response_dict.get("assignment_type"))
modulestore().update_item(self.course, self.staff_user.id)
updated_name = random_cohort.name + "_updated" # Update the newly created random cohort
response_dict = self.put_cohort_handler( newly_created_cohort = get_cohort_by_name(self.course.id, cohort_name)
self.course, random_cohort, {'name': updated_name}, expected_response_code=400 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)
self.assertEqual(cohort_name, get_cohort_by_id(self.course.id, newly_created_cohort.id).name)
self.assertEqual(cohort_name, response_dict.get("name"))
self.assertEqual(CourseCohort.RANDOM, response_dict.get("assignment_type"))
def test_cannot_update_assignment_type_of_single_random_cohort(self):
"""
Test that it is not possible to update the assignment type of a single random cohort.
"""
# 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)
self.assertEqual(cohort_name, response_dict.get("name"))
self.assertEqual(CourseCohort.RANDOM, response_dict.get("assignment_type"))
# 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)
self.assertEqual( self.assertEqual(
"Renaming of random cohorts is not supported at this time.", response_dict.get("error") 'There must be one cohort to which students can be randomly assigned.', response_dict.get("error")
) )
self.assertEqual(random_cohort_name, get_cohort_by_id(self.course.id, random_cohort.id).name)
self.assertEqual(CohortAssignmentType.RANDOM, CohortAssignmentType.get(random_cohort, self.course))
def test_update_cohort_group_id(self): def test_update_cohort_group_id(self):
""" """
...@@ -365,9 +413,13 @@ class CohortHandlerTestCase(CohortViewsTestCase): ...@@ -365,9 +413,13 @@ class CohortHandlerTestCase(CohortViewsTestCase):
""" """
self._create_cohorts() self._create_cohorts()
self.assertEqual((None, None), get_group_info_for_cohort(self.cohort1)) self.assertEqual((None, None), get_group_info_for_cohort(self.cohort1))
response_dict = self.put_cohort_handler( data = {
self.course, self.cohort1, data={'name': self.cohort1.name, 'group_id': 2, 'user_partition_id': 3} 'name': self.cohort1.name,
) 'assignment_type': CourseCohort.MANUAL,
'group_id': 2,
'user_partition_id': 3
}
response_dict = self.put_cohort_handler(self.course, self.cohort1, data=data)
self.assertEqual((2, 3), get_group_info_for_cohort(self.cohort1)) self.assertEqual((2, 3), get_group_info_for_cohort(self.cohort1))
self.assertEqual(2, response_dict.get("group_id")) self.assertEqual(2, response_dict.get("group_id"))
self.assertEqual(3, response_dict.get("user_partition_id")) self.assertEqual(3, response_dict.get("user_partition_id"))
...@@ -381,9 +433,8 @@ class CohortHandlerTestCase(CohortViewsTestCase): ...@@ -381,9 +433,8 @@ class CohortHandlerTestCase(CohortViewsTestCase):
self._create_cohorts() self._create_cohorts()
link_cohort_to_partition_group(self.cohort1, 5, 0) link_cohort_to_partition_group(self.cohort1, 5, 0)
self.assertEqual((0, 5), get_group_info_for_cohort(self.cohort1)) self.assertEqual((0, 5), get_group_info_for_cohort(self.cohort1))
response_dict = self.put_cohort_handler( data = {'name': self.cohort1.name, 'assignment_type': CourseCohort.RANDOM, 'group_id': None}
self.course, self.cohort1, data={'name': self.cohort1.name, 'group_id': None} response_dict = self.put_cohort_handler(self.course, self.cohort1, data=data)
)
self.assertEqual((None, None), get_group_info_for_cohort(self.cohort1)) self.assertEqual((None, None), get_group_info_for_cohort(self.cohort1))
self.assertIsNone(response_dict.get("group_id")) self.assertIsNone(response_dict.get("group_id"))
self.assertIsNone(response_dict.get("user_partition_id")) self.assertIsNone(response_dict.get("user_partition_id"))
...@@ -394,24 +445,32 @@ class CohortHandlerTestCase(CohortViewsTestCase): ...@@ -394,24 +445,32 @@ class CohortHandlerTestCase(CohortViewsTestCase):
different group_id. different group_id.
""" """
self._create_cohorts() self._create_cohorts()
self.assertEqual((None, None), get_group_info_for_cohort(self.cohort1)) self.assertEqual((None, None), get_group_info_for_cohort(self.cohort4))
self.put_cohort_handler( data = {
self.course, self.cohort1, data={'name': self.cohort1.name, 'group_id': 2, 'user_partition_id': 3} 'name': self.cohort4.name,
) 'assignment_type': CourseCohort.RANDOM,
self.assertEqual((2, 3), get_group_info_for_cohort(self.cohort1)) 'group_id': 2,
self.put_cohort_handler( 'user_partition_id': 3
self.course, self.cohort1, data={'name': self.cohort1.name, 'group_id': 1, 'user_partition_id': 3} }
) self.put_cohort_handler(self.course, self.cohort4, data=data)
self.assertEqual((1, 3), get_group_info_for_cohort(self.cohort1)) self.assertEqual((2, 3), get_group_info_for_cohort(self.cohort4))
data = {
'name': self.cohort4.name,
'assignment_type': CourseCohort.RANDOM,
'group_id': 1,
'user_partition_id': 3
}
self.put_cohort_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): def test_update_cohort_missing_user_partition_id(self):
""" """
Verify that we cannot update a cohort with a group_id if the user_partition_id is not also specified. Verify that we cannot update a cohort with a group_id if the user_partition_id is not also specified.
""" """
self._create_cohorts() self._create_cohorts()
response_dict = self.put_cohort_handler( data = {'name': self.cohort1.name, 'assignment_type': CourseCohort.RANDOM, 'group_id': 2}
self.course, self.cohort1, data={'name': self.cohort1.name, 'group_id': 2}, expected_response_code=400 response_dict = self.put_cohort_handler(self.course, self.cohort1, data=data, expected_response_code=400)
)
self.assertEqual( self.assertEqual(
"If group_id is specified, user_partition_id must also be specified.", response_dict.get("error") "If group_id is specified, user_partition_id must also be specified.", response_dict.get("error")
) )
......
...@@ -7,7 +7,8 @@ from django.http import Http404, HttpResponse, HttpResponseBadRequest ...@@ -7,7 +7,8 @@ from django.http import Http404, HttpResponse, HttpResponseBadRequest
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from util.json_request import expect_json, JsonResponse from util.json_request import expect_json, JsonResponse
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
import json from django.utils.translation import ugettext
import logging import logging
import re import re
...@@ -59,13 +60,14 @@ def _get_cohort_representation(cohort, course): ...@@ -59,13 +60,14 @@ def _get_cohort_representation(cohort, course):
Returns a JSON representation of a cohort. Returns a JSON representation of a cohort.
""" """
group_id, partition_id = cohorts.get_group_info_for_cohort(cohort) group_id, partition_id = cohorts.get_group_info_for_cohort(cohort)
assignment_type = cohorts.get_assignment_type(cohort)
return { return {
'name': cohort.name, 'name': cohort.name,
'id': cohort.id, 'id': cohort.id,
'user_count': cohort.users.count(), 'user_count': cohort.users.count(),
'assignment_type': cohorts.CohortAssignmentType.get(cohort, course), 'assignment_type': assignment_type,
'user_partition_id': partition_id, 'user_partition_id': partition_id,
'group_id': group_id 'group_id': group_id,
} }
...@@ -102,25 +104,30 @@ def cohort_handler(request, course_key_string, cohort_id=None): ...@@ -102,25 +104,30 @@ def cohort_handler(request, course_key_string, cohort_id=None):
cohort = cohorts.get_cohort_by_id(course_key, cohort_id) cohort = cohorts.get_cohort_by_id(course_key, cohort_id)
return JsonResponse(_get_cohort_representation(cohort, course)) return JsonResponse(_get_cohort_representation(cohort, course))
else: else:
name = request.json.get('name')
assignment_type = request.json.get('assignment_type')
if not name:
# Note: error message not translated because it is not exposed to the user (UI prevents this state).
return JsonResponse({"error": "Cohort name must be specified."}, 400)
if not assignment_type:
# Note: error message not translated because it is not exposed to the user (UI prevents this state).
return JsonResponse({"error": "Assignment type must be specified."}, 400)
# If cohort_id is specified, update the existing cohort. Otherwise, create a new cohort. # If cohort_id is specified, update the existing cohort. Otherwise, create a new cohort.
if cohort_id: if cohort_id:
cohort = cohorts.get_cohort_by_id(course_key, cohort_id) cohort = cohorts.get_cohort_by_id(course_key, cohort_id)
name = request.json.get('name')
if name != cohort.name: if name != cohort.name:
if cohorts.CohortAssignmentType.get(cohort, course) == cohorts.CohortAssignmentType.RANDOM: if cohorts.is_cohort_exists(course_key, name):
return JsonResponse( err_msg = ugettext("A cohort with the same name already exists.")
# Note: error message not translated because it is not exposed to the user (UI prevents). return JsonResponse({"error": unicode(err_msg)}, 400)
{"error": "Renaming of random cohorts is not supported at this time."}, 400
)
cohort.name = name cohort.name = name
cohort.save() cohort.save()
try:
cohorts.set_assignment_type(cohort, assignment_type)
except ValueError as err:
return JsonResponse({"error": unicode(err)}, 400)
else: else:
name = request.json.get('name')
if not name:
# Note: error message not translated because it is not exposed to the user (UI prevents this state).
return JsonResponse({"error": "In order to create a cohort, a name must be specified."}, 400)
try: try:
cohort = cohorts.add_cohort(course_key, name) cohort = cohorts.add_cohort(course_key, name, assignment_type)
except ValueError as err: except ValueError as err:
return JsonResponse({"error": unicode(err)}, 400) return JsonResponse({"error": unicode(err)}, 400)
......
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