Commit 6219ecac by Andy Armstrong Committed by cahrens

Support adding cohorts from the instructor dashboard

TNL-162
parent b58f7ad7
...@@ -5,6 +5,12 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,6 +5,12 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. the top. Include a label indicating the component affected.
LMS: Support adding cohorts from the instructor dashboard. TNL-162
LMS: Support adding students to a cohort via the instructor dashboard. TNL-163
LMS: Show cohorts on the new instructor dashboard. TNL-161
LMS: Mobile API available for courses that opt in using the Course Advanced LMS: Mobile API available for courses that opt in using the Course Advanced
Setting "Mobile Course Available" (only used in limited closed beta). Setting "Mobile Course Available" (only used in limited closed beta).
......
...@@ -3,11 +3,12 @@ This file contains the logic for cohort groups, as exposed internally to the ...@@ -3,11 +3,12 @@ This file contains the logic for cohort groups, as exposed internally to the
forums, and to the cohort admin views. forums, and to the cohort admin views.
""" """
from django.http import Http404
import logging import logging
import random import random
from django.http import Http404
from django.utils.translation import ugettext as _
from courseware import courses from courseware import courses
from student.models import get_user_by_username_or_email from student.models import get_user_by_username_or_email
from .models import CourseUserGroup from .models import CourseUserGroup
...@@ -140,7 +141,7 @@ def get_cohorted_commentables(course_key): ...@@ -140,7 +141,7 @@ def get_cohorted_commentables(course_key):
def get_cohort(user, course_key): def get_cohort(user, course_key):
""" """
Given a django User and a CourseKey, return the user's cohort in that Given a Django user and a CourseKey, return the user's cohort in that
cohort. cohort.
Arguments: Arguments:
...@@ -180,7 +181,7 @@ def get_cohort(user, course_key): ...@@ -180,7 +181,7 @@ def get_cohort(user, course_key):
# Use the "default cohort". # Use the "default cohort".
group_name = DEFAULT_COHORT_NAME group_name = DEFAULT_COHORT_NAME
group, _ = CourseUserGroup.objects.get_or_create( group, __ = CourseUserGroup.objects.get_or_create(
course_id=course_key, course_id=course_key,
group_type=CourseUserGroup.COHORT, group_type=CourseUserGroup.COHORT,
name=group_name name=group_name
...@@ -250,7 +251,7 @@ def add_cohort(course_key, name): ...@@ -250,7 +251,7 @@ def add_cohort(course_key, name):
if CourseUserGroup.objects.filter(course_id=course_key, if CourseUserGroup.objects.filter(course_id=course_key,
group_type=CourseUserGroup.COHORT, group_type=CourseUserGroup.COHORT,
name=name).exists(): name=name).exists():
raise ValueError("Can't create two cohorts with the same name") raise ValueError(_("You cannot create two cohorts with the same name"))
try: try:
course = courses.get_course_by_id(course_key) course = courses.get_course_by_id(course_key)
...@@ -296,9 +297,10 @@ def add_user_to_cohort(cohort, username_or_email): ...@@ -296,9 +297,10 @@ def add_user_to_cohort(cohort, username_or_email):
) )
if course_cohorts.exists(): if course_cohorts.exists():
if course_cohorts[0] == cohort: if course_cohorts[0] == cohort:
raise ValueError("User {0} already present in cohort {1}".format( raise ValueError("User {user_name} already present in cohort {cohort_name}".format(
user.username, user_name=user.username,
cohort.name)) cohort_name=cohort.name
))
else: else:
previous_cohort = course_cohorts[0].name previous_cohort = course_cohorts[0].name
course_cohorts[0].users.remove(user) course_cohorts[0].users.remove(user)
...@@ -313,8 +315,9 @@ def delete_empty_cohort(course_key, name): ...@@ -313,8 +315,9 @@ def delete_empty_cohort(course_key, name):
""" """
cohort = get_cohort_by_name(course_key, name) cohort = get_cohort_by_name(course_key, name)
if cohort.users.exists(): if cohort.users.exists():
raise ValueError( raise ValueError(_("You cannot delete non-empty cohort {cohort_name} in course {course_key}").format(
"Can't delete non-empty cohort {0} in course {1}".format( cohort_name=name,
name, course_key)) course_key=course_key
))
cohort.delete() cohort.delete()
...@@ -9,6 +9,9 @@ from xmodule.modulestore import ModuleStoreEnum ...@@ -9,6 +9,9 @@ from xmodule.modulestore import ModuleStoreEnum
class CohortFactory(DjangoModelFactory): class CohortFactory(DjangoModelFactory):
"""
Factory for constructing mock cohorts.
"""
FACTORY_FOR = CourseUserGroup FACTORY_FOR = CourseUserGroup
name = Sequence("cohort{}".format) name = Sequence("cohort{}".format)
...@@ -17,6 +20,9 @@ class CohortFactory(DjangoModelFactory): ...@@ -17,6 +20,9 @@ class CohortFactory(DjangoModelFactory):
@post_generation @post_generation
def users(self, create, extracted, **kwargs): # pylint: disable=W0613 def users(self, create, extracted, **kwargs): # pylint: disable=W0613
"""
Returns the users associated with the cohort.
"""
if extracted: if extracted:
self.users.add(*extracted) self.users.add(*extracted)
......
...@@ -277,7 +277,7 @@ class AddCohortTestCase(CohortViewsTestCase): ...@@ -277,7 +277,7 @@ class AddCohortTestCase(CohortViewsTestCase):
self.verify_contains_added_cohort( self.verify_contains_added_cohort(
self.request_add_cohort(cohort_name, self.course), self.request_add_cohort(cohort_name, self.course),
cohort_name, cohort_name,
expected_error_msg="Can't create two cohorts with the same name" expected_error_msg="You cannot create two cohorts with the same name"
) )
......
...@@ -37,7 +37,7 @@ class MembershipPage(PageObject): ...@@ -37,7 +37,7 @@ class MembershipPage(PageObject):
def _get_cohort_options(self): def _get_cohort_options(self):
""" """
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 group".
""" """
return self.q(css=".cohort-management #cohort-select option") return self.q(css=".cohort-management #cohort-select option")
...@@ -55,7 +55,7 @@ class MembershipPage(PageObject): ...@@ -55,7 +55,7 @@ class MembershipPage(PageObject):
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 group".
""" """
return [ return [
self._cohort_name(opt.text) self._cohort_name(opt.text)
...@@ -86,6 +86,15 @@ class MembershipPage(PageObject): ...@@ -86,6 +86,15 @@ class MembershipPage(PageObject):
lambda el: self._cohort_name(el.text) == cohort_name lambda el: self._cohort_name(el.text) == cohort_name
).first.click() ).first.click()
def add_cohort(self, cohort_name):
"""
Adds a new manual cohort with the specified name.
"""
self.q(css="div.cohort-management-nav .action-create").first.click()
textinput = self.q(css="#cohort-create-name").results[0]
textinput.send_keys(cohort_name)
self.q(css="div.form-actions .action-save").first.click()
def get_cohort_group_setup(self): def get_cohort_group_setup(self):
""" """
Returns the description of the current cohort Returns the description of the current cohort
......
...@@ -11,6 +11,8 @@ from ...pages.lms.auto_auth import AutoAuthPage ...@@ -11,6 +11,8 @@ from ...pages.lms.auto_auth import AutoAuthPage
from ...pages.lms.instructor_dashboard import InstructorDashboardPage from ...pages.lms.instructor_dashboard import InstructorDashboardPage
from ...pages.studio.settings_advanced import AdvancedSettingsPage from ...pages.studio.settings_advanced import AdvancedSettingsPage
import uuid
class CohortConfigurationTest(UniqueCourseTest, CohortTestMixin): class CohortConfigurationTest(UniqueCourseTest, CohortTestMixin):
""" """
...@@ -149,6 +151,30 @@ class CohortConfigurationTest(UniqueCourseTest, CohortTestMixin): ...@@ -149,6 +151,30 @@ class CohortConfigurationTest(UniqueCourseTest, CohortTestMixin):
self.assertEqual("There was an error when trying to add students:", error_messages[0]) self.assertEqual("There was an error when trying to add students:", error_messages[0])
self.assertEqual("Unknown user: unknown_user", error_messages[1]) self.assertEqual("Unknown user: unknown_user", error_messages[1])
self.assertEqual( self.assertEqual(
self.student_name+",unknown_user,", self.student_name + ",unknown_user,",
self.membership_page.get_cohort_student_input_field_value() self.membership_page.get_cohort_student_input_field_value()
) )
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 when I add the user to the new cohort
Then the cohort has 1 user
"""
new_cohort = str(uuid.uuid4().get_hex()[0:20])
self.assertFalse(new_cohort in self.membership_page.get_cohorts())
self.membership_page.add_cohort(new_cohort)
# After adding the cohort, it should automatically be selected
EmptyPromise(
lambda: new_cohort == self.membership_page.get_selected_cohort(), "Waiting for new cohort to appear"
).fulfill()
self.assertEqual(0, self.membership_page.get_selected_cohort_count())
self.membership_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.
EmptyPromise(
lambda: 1 == self.membership_page.get_selected_cohort_count(), 'Waiting for student to be added'
).fulfill()
...@@ -992,6 +992,7 @@ courseware_js = ( ...@@ -992,6 +992,7 @@ courseware_js = (
base_vendor_js = [ base_vendor_js = [
'js/vendor/jquery.min.js', 'js/vendor/jquery.min.js',
'js/vendor/jquery.cookie.js', 'js/vendor/jquery.cookie.js',
'js/vendor/underscore-min.js'
] ]
main_vendor_js = base_vendor_js + [ main_vendor_js = base_vendor_js + [
...@@ -999,7 +1000,6 @@ main_vendor_js = base_vendor_js + [ ...@@ -999,7 +1000,6 @@ main_vendor_js = base_vendor_js + [
'js/RequireJS-namespace-undefine.js', 'js/RequireJS-namespace-undefine.js',
'js/vendor/json2.js', 'js/vendor/json2.js',
'js/vendor/jquery-ui.min.js', 'js/vendor/jquery-ui.min.js',
'js/vendor/jquery.cookie.js',
'js/vendor/jquery.qtip.min.js', 'js/vendor/jquery.qtip.min.js',
'js/vendor/swfobject/swfobject.js', 'js/vendor/swfobject/swfobject.js',
'js/vendor/jquery.ba-bbq.min.js', 'js/vendor/jquery.ba-bbq.min.js',
......
(function(Backbone) { (function(Backbone) {
var NotificationModel = Backbone.Model.extend({ var NotificationModel = Backbone.Model.extend({
defaults: { defaults: {
// Supported types are "confirmation" and "error". /**
* The type of notification to be shown.
* Supported types are "confirmation", "warning" and "error".
*/
type: "confirmation", type: "confirmation",
/**
* The title to be shown for the notification. This string should be short so
* that it can be shown on a single line.
*/
title: "", title: "",
/**
* An optional message giving more details for the notification. This string can be as long
* as needed and will wrap.
*/
message: "",
/**
* An optional array of detail messages to be shown beneath the title and message. This is
* typically used to enumerate a set of warning or error conditions that occurred.
*/
details: [], details: [],
actionText: "", /**
* The text label to be shown for an action button, or null if there is no associated action.
*/
actionText: null,
/**
* The class to be added to the action button. This allows selectors to be written that can
* target the action button directly.
*/
actionClass: "",
/**
* An optional icon class to be shown before the text on the action button.
*/
actionIconClass: "",
/**
* An optional callback that will be invoked when the user clicks on the action button.
*/
actionCallback: null actionCallback: null
} }
}); });
......
define(['backbone', 'jquery', 'js/models/notification', 'js/views/notification', 'js/common_helpers/template_helpers'], define(['backbone', 'jquery', 'js/models/notification', 'js/views/notification', 'js/common_helpers/template_helpers'],
function (Backbone, $, NotificationModel, NotificationView, TemplateHelpers) { function (Backbone, $, NotificationModel, NotificationView, TemplateHelpers) {
describe("NotificationView", function () { describe("NotificationView", function () {
var createNotification, verifyTitle, verifyDetails, verifyAction, notificationView; var createNotification, verifyTitle, verifyMessage, verifyDetails, verifyAction, notificationView;
createNotification = function (modelVals) { createNotification = function (modelVals) {
var notificationModel = new NotificationModel(modelVals); var notificationModel = new NotificationModel(modelVals);
...@@ -16,6 +16,10 @@ define(['backbone', 'jquery', 'js/models/notification', 'js/views/notification', ...@@ -16,6 +16,10 @@ define(['backbone', 'jquery', 'js/models/notification', 'js/views/notification',
expect(notificationView.$('.message-title').text().trim()).toBe(expectedTitle); expect(notificationView.$('.message-title').text().trim()).toBe(expectedTitle);
}; };
verifyMessage = function (expectedMessage) {
expect(notificationView.$('.message-copy').text().trim()).toBe(expectedMessage);
};
verifyDetails = function (expectedDetails) { verifyDetails = function (expectedDetails) {
var details = notificationView.$('.summary-item'); var details = notificationView.$('.summary-item');
expect(details.length).toBe(expectedDetails.length); expect(details.length).toBe(expectedDetails.length);
...@@ -41,7 +45,7 @@ define(['backbone', 'jquery', 'js/models/notification', 'js/views/notification', ...@@ -41,7 +45,7 @@ define(['backbone', 'jquery', 'js/models/notification', 'js/views/notification',
it('has default values', function () { it('has default values', function () {
createNotification({}); createNotification({});
expect(notificationView.$('div.message').hasClass('message-confirmation')).toBe(true); expect(notificationView.$('div.message')).toHaveClass('message-confirmation');
verifyTitle(''); verifyTitle('');
verifyDetails([]); verifyDetails([]);
verifyAction(null); verifyAction(null);
...@@ -49,8 +53,8 @@ define(['backbone', 'jquery', 'js/models/notification', 'js/views/notification', ...@@ -49,8 +53,8 @@ define(['backbone', 'jquery', 'js/models/notification', 'js/views/notification',
it('can use an error type', function () { it('can use an error type', function () {
createNotification({type: 'error'}); createNotification({type: 'error'});
expect(notificationView.$('div.message').hasClass('message-error')).toBe(true); expect(notificationView.$('div.message')).toHaveClass('message-error');
expect(notificationView.$('div.message').hasClass('message-confirmation')).toBe(false); expect(notificationView.$('div.message')).not.toHaveClass('message-confirmation');
}); });
it('can specify a title', function () { it('can specify a title', function () {
...@@ -58,6 +62,11 @@ define(['backbone', 'jquery', 'js/models/notification', 'js/views/notification', ...@@ -58,6 +62,11 @@ define(['backbone', 'jquery', 'js/models/notification', 'js/views/notification',
verifyTitle('notification title'); verifyTitle('notification title');
}); });
it('can specify a message', function () {
createNotification({message: 'This is a dummy message'});
verifyMessage('This is a dummy message');
});
it('can specify details', function () { it('can specify details', function () {
var expectedDetails = ['detail 1', 'detail 2']; var expectedDetails = ['detail 1', 'detail 2'];
createNotification({details: expectedDetails}); createNotification({details: expectedDetails});
...@@ -69,9 +78,9 @@ define(['backbone', 'jquery', 'js/models/notification', 'js/views/notification', ...@@ -69,9 +78,9 @@ define(['backbone', 'jquery', 'js/models/notification', 'js/views/notification',
verifyAction('action text'); verifyAction('action text');
}); });
it ('does not show an action button if callback is not provided', function () { it ('shows an action button if only text is provided', function () {
createNotification({actionText: 'action text'}); createNotification({actionText: 'action text'});
verifyAction(null); verifyAction('action text');
}); });
it ('does not show an action button if text is not provided', function () { it ('does not show an action button if text is not provided', function () {
...@@ -82,7 +91,7 @@ define(['backbone', 'jquery', 'js/models/notification', 'js/views/notification', ...@@ -82,7 +91,7 @@ define(['backbone', 'jquery', 'js/models/notification', 'js/views/notification',
it ('triggers the callback when the action button is clicked', function () { it ('triggers the callback when the action button is clicked', function () {
var actionCallback = jasmine.createSpy('Spy on callback'); var actionCallback = jasmine.createSpy('Spy on callback');
var view = createNotification({actionText: 'action text', actionCallback: actionCallback}); var view = createNotification({actionText: 'action text', actionCallback: actionCallback});
notificationView.$('a.action-primary').click(); notificationView.$('button.action-primary').click();
expect(actionCallback).toHaveBeenCalledWith(view); expect(actionCallback).toHaveBeenCalledWith(view);
}); });
}); });
......
...@@ -101,7 +101,8 @@ ...@@ -101,7 +101,8 @@
addNotifications: function(modifiedUsers) { addNotifications: function(modifiedUsers) {
var oldCohort, title, details, numPresent, numUsersAdded, numErrors, var oldCohort, title, details, numPresent, numUsersAdded, numErrors,
createErrorDetails, errorActionCallback, errorModel; createErrorDetails, errorActionCallback, errorModel,
errorLimit = 5;
// Show confirmation messages. // Show confirmation messages.
this.undelegateViewEvents(this.confirmationNotifications); this.undelegateViewEvents(this.confirmationNotifications);
...@@ -165,7 +166,7 @@ ...@@ -165,7 +166,7 @@
numErrors = modifiedUsers.unknown.length; numErrors = modifiedUsers.unknown.length;
if (numErrors > 0) { if (numErrors > 0) {
createErrorDetails = function (unknownUsers, showAllErrors) { createErrorDetails = function (unknownUsers, showAllErrors) {
var numErrors = unknownUsers.length, details = [], errorLimit = 5; var numErrors = unknownUsers.length, details = [];
for (var i = 0; i < (showAllErrors ? numErrors : Math.min(errorLimit, numErrors)); i++) { for (var i = 0; i < (showAllErrors ? numErrors : Math.min(errorLimit, numErrors)); i++) {
details.push(interpolate_text(gettext("Unknown user: {user}"), {user: unknownUsers[i]})); details.push(interpolate_text(gettext("Unknown user: {user}"), {user: unknownUsers[i]}));
...@@ -181,15 +182,16 @@ ...@@ -181,15 +182,16 @@
details = createErrorDetails(modifiedUsers.unknown, false); details = createErrorDetails(modifiedUsers.unknown, false);
errorActionCallback = function (view) { errorActionCallback = function (view) {
view.model.set("actionCallback", null); view.model.set("actionText", null);
view.model.set("details", createErrorDetails(modifiedUsers.unknown, true)); view.model.set("details", createErrorDetails(modifiedUsers.unknown, true));
view.render(); view.render();
}; };
errorModel = new NotificationModel({ errorModel = new NotificationModel({
details: details, details: details,
actionText: gettext("View all errors"), actionText: numErrors > errorLimit ? gettext("View all errors") : null,
actionCallback: numErrors > 5 ? errorActionCallback : null actionCallback: errorActionCallback,
actionClass: 'action-expand'
}); });
this.showErrorMessage(title, false, errorModel); this.showErrorMessage(title, false, errorModel);
......
(function(Backbone, CohortEditorView) { (function($, _, Backbone, gettext, interpolate_text, CohortEditorView, NotificationModel, NotificationView) {
var CohortsView = Backbone.View.extend({ var hiddenClass = 'is-hidden',
disabledClass = 'is-disabled';
this.CohortsView = Backbone.View.extend({
events : { events : {
"change .cohort-select": "showCohortEditor" 'change .cohort-select': 'onCohortSelected',
'click .action-create': 'showAddCohortForm',
'click .action-cancel': 'cancelAddCohortForm',
'click .action-save': 'saveAddCohortForm'
}, },
initialize: function(options) { initialize: function(options) {
this.template = _.template($('#cohorts-tpl').text()); this.template = _.template($('#cohorts-tpl').text());
this.selectorTemplate = _.template($('#cohort-selector-tpl').text()); this.selectorTemplate = _.template($('#cohort-selector-tpl').text());
this.addCohortFormTemplate = _.template($('#add-cohort-form-tpl').text());
this.advanced_settings_url = options.advanced_settings_url; this.advanced_settings_url = options.advanced_settings_url;
this.model.on('sync', this.onSync, this); this.model.on('sync', this.onSync, this);
}, },
...@@ -15,7 +22,7 @@ ...@@ -15,7 +22,7 @@
this.$el.html(this.template({ this.$el.html(this.template({
cohorts: this.model.models cohorts: this.model.models
})); }));
this.renderSelector(); this.onSync();
return this; return this;
}, },
...@@ -27,7 +34,25 @@ ...@@ -27,7 +34,25 @@
}, },
onSync: function() { onSync: function() {
this.renderSelector(this.getSelectedCohort()); var selectedCohort = this.lastSelectedCohortId && this.model.get(this.lastSelectedCohortId),
hasCohorts = this.model.length > 0;
this.hideAddCohortForm();
if (hasCohorts) {
this.$('.cohort-management-nav').removeClass(hiddenClass);
this.renderSelector(selectedCohort);
if (selectedCohort) {
this.showCohortEditor(selectedCohort);
}
} else {
this.$('.cohort-management-nav').addClass(hiddenClass);
this.showNotification({
type: 'warning',
title: gettext('You currently have no cohort groups configured'),
actionText: gettext('Add Cohort Group'),
actionClass: 'action-create',
actionIconClass: 'icon-plus'
});
}
}, },
getSelectedCohort: function() { getSelectedCohort: function() {
...@@ -35,22 +60,116 @@ ...@@ -35,22 +60,116 @@
return id && this.model.get(parseInt(id)); return id && this.model.get(parseInt(id));
}, },
showCohortEditor: function(event) { onCohortSelected: function(event) {
event.preventDefault(); event.preventDefault();
var selectedCohort = this.getSelectedCohort(); var selectedCohort = this.getSelectedCohort();
this.lastSelectedCohortId = selectedCohort.get('id');
this.showCohortEditor(selectedCohort);
},
showCohortEditor: function(cohort) {
this.removeNotification();
if (this.editor) { if (this.editor) {
this.editor.setCohort(selectedCohort); this.editor.setCohort(cohort);
} else { } else {
this.editor = new CohortEditorView({ this.editor = new CohortEditorView({
el: this.$('.cohort-management-group'), el: this.$('.cohort-management-group'),
model: selectedCohort, model: cohort,
cohorts: this.model, cohorts: this.model,
advanced_settings_url: this.advanced_settings_url advanced_settings_url: this.advanced_settings_url
}); });
this.editor.render(); this.editor.render();
} }
},
showNotification: function(options, beforeElement) {
var model = new NotificationModel(options);
this.removeNotification();
this.notification = new NotificationView({
model: model
});
this.notification.render();
if (!beforeElement) {
beforeElement = this.$('.cohort-management-group');
}
beforeElement.before(this.notification.$el);
},
removeNotification: function() {
if (this.notification) {
this.notification.remove();
}
},
showAddCohortForm: function(event) {
event.preventDefault();
this.removeNotification();
this.addCohortForm = $(this.addCohortFormTemplate({}));
this.addCohortForm.insertAfter(this.$('.cohort-management-nav'));
this.setCohortEditorVisibility(false);
},
hideAddCohortForm: function() {
this.setCohortEditorVisibility(true);
if (this.addCohortForm) {
this.addCohortForm.remove();
this.addCohortForm = null;
}
},
setCohortEditorVisibility: function(showEditor) {
if (showEditor) {
this.$('.cohort-management-group').removeClass(hiddenClass);
this.$('.cohort-management-nav').removeClass(disabledClass);
} else {
this.$('.cohort-management-group').addClass(hiddenClass);
this.$('.cohort-management-nav').addClass(disabledClass);
}
},
saveAddCohortForm: function(event) {
event.preventDefault();
var self = this,
showAddError,
cohortName = this.$('.cohort-create-name').val().trim();
showAddError = function(message) {
self.showNotification(
{type: 'error', title: message},
self.$('.cohort-management-create-form-name label')
);
};
this.removeNotification();
if (cohortName.length > 0) {
$.post(
this.model.url + '/add',
{name: cohortName}
).done(function(result) {
if (result.success) {
self.lastSelectedCohortId = result.cohort.id;
self.model.fetch().done(function() {
self.showNotification({
type: 'confirmation',
title: interpolate_text(
gettext('The {cohortGroupName} cohort group has been created. You can manually add students to this group below.'),
{cohortGroupName: cohortName}
)
});
});
} else {
showAddError(result.msg);
}
}).fail(function() {
showAddError(gettext("We've encountered an error. Please refresh your browser and then try again."));
});
} else {
showAddError(gettext('Please enter a name for your new cohort group.'));
}
},
cancelAddCohortForm: function(event) {
event.preventDefault();
this.removeNotification();
this.onSync();
} }
}); });
}).call(this, $, _, Backbone, gettext, interpolate_text, CohortEditorView, NotificationModel, NotificationView);
this.CohortsView = CohortsView;
}).call(this, Backbone, CohortEditorView);
...@@ -9,17 +9,14 @@ ...@@ -9,17 +9,14 @@
}, },
render: function() { render: function() {
var actionText = null;
// If no actionText is passed to the template, the action button
// will not be shown.
if (this.model.get("actionText") && this.model.get("actionCallback")) {
actionText = this.model.get("actionText");
}
this.$el.html(this.template({ this.$el.html(this.template({
type: this.model.get("type"), type: this.model.get("type"),
title: this.model.get("title"), title: this.model.get("title"),
message: this.model.get("message"),
details: this.model.get("details"), details: this.model.get("details"),
actionText: actionText actionText: this.model.get("actionText"),
actionClass: this.model.get("actionClass"),
actionIconClass: this.model.get("actionIconClass")
})); }));
return this; return this;
}, },
......
...@@ -454,8 +454,11 @@ section.instructor-dashboard-content-2 { ...@@ -454,8 +454,11 @@ section.instructor-dashboard-content-2 {
.form-submit { .form-submit {
@include idashbutton($blue); @include idashbutton($blue);
@include font-size(14);
@include line-height(14);
margin-right: ($baseline/2); margin-right: ($baseline/2);
margin-bottom: 0; margin-bottom: 0;
text-shadow: none;
} }
.form-cancel { .form-cancel {
...@@ -481,6 +484,21 @@ section.instructor-dashboard-content-2 { ...@@ -481,6 +484,21 @@ section.instructor-dashboard-content-2 {
@include idashbutton($blue); @include idashbutton($blue);
float: right; float: right;
text-align: right; text-align: right;
text-shadow: none;
font-weight: 600;
}
// STATE: is disabled
&.is-disabled {
.cohort-select {
opacity: 0.25;
}
.action-create {
opacity: 0.50;
}
} }
} }
......
...@@ -11,7 +11,6 @@ ...@@ -11,7 +11,6 @@
<script type="text/javascript" src="${static.url('js/src/jquery.timeago.locale.js')}"></script> <script type="text/javascript" src="${static.url('js/src/jquery.timeago.locale.js')}"></script>
<script type="text/javascript" src="${static.url('js/mustache.js')}"></script> <script type="text/javascript" src="${static.url('js/mustache.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/URI.min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/URI.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/src/tooltip_manager.js')}"></script> <script type="text/javascript" src="${static.url('js/src/tooltip_manager.js')}"></script>
......
<div class="cohort-management-create">
<form action="" method="post" name="" id="cohort-management-create-form" class="cohort-management-create-form">
<h3 class="form-title"><%- gettext('Add a New Cohort Group') %></h3>
<div class="form-fields">
<div class="cohort-management-create-form-name field field-text">
<label for="cohort-create-name" class="label">
<%- gettext('New Cohort Name') %> *
<span class="sr"><%- gettext('(Required Field)')%></span>
</label>
<input type="text" name="cohort-create-name" value="" class="input cohort-create-name"
id="cohort-create-name"
placeholder="<%- gettext("Enter Your New Cohort Group's Name") %>" required="required" />
</div>
</div>
<div class="form-actions">
<button class="form-submit button action-primary action-save">
<i class="icon icon-plus"></i>
<%- gettext('Save') %>
</button>
<a href="" class="form-cancel action-secondary action-cancel"><%- gettext('Cancel') %></a>
</div>
</form>
</div>
<option value=""><%- gettext('Select a cohort') %></option> <% if (!selectedCohort) { %>
<option value=""><%- gettext('Select a cohort group') %></option>
<% } %>
<% _.each(cohorts, function(cohort) { %> <% _.each(cohorts, function(cohort) { %>
<% <%
var label = interpolate( var label = interpolate(
......
...@@ -4,19 +4,24 @@ ...@@ -4,19 +4,24 @@
</h2> </h2>
<!-- nav --> <!-- nav -->
<% if (cohorts.length > 0) { %> <div class="cohort-management-nav">
<div class="cohort-management-nav"> <form action="" method="post" name="" id="cohort-management-nav-form" class="cohort-management-nav-form">
<form action="" method="post" name="" id="cohort-management-nav-form" class="cohort-management-nav-form"> <div class="cohort-management-nav-form-select field field-select">
<div class="cohort-management-nav-form-select field field-select"> <label for="cohort-select" class="label sr">${_("Select a cohort group to manage")}</label>
<label for="cohort-select" class="label sr">${_("Select a cohort to manage")}</label> <select class="input cohort-select" name="cohort-select" id="cohort-select">
<select class="input cohort-select" name="cohort-select" id="cohort-select"> </select>
</select> </div>
</div>
<button class="form-submit button action-primary action-view sr"><%- gettext('View cohort group') %></button> <div class="form-actions">
</form> <button class="form-submit button action-primary action-view sr"><%- gettext('View Cohort Group') %></button>
</div> </div>
</form>
<!-- individual group --> <a href="" class="action-primary action-create">
<div class="cohort-management-group"></div> <i class="icon icon-plus"></i>
<% } %> <%- gettext('Add Cohort Group') %>
</a>
</div>
<!-- individual group -->
<div class="cohort-management-group"></div>
...@@ -32,7 +32,6 @@ ...@@ -32,7 +32,6 @@
window.Range.prototype = { }; window.Range.prototype = { };
} }
</script> </script>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/mustache.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/mustache.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
...@@ -63,7 +62,7 @@ ...@@ -63,7 +62,7 @@
## Include Underscore templates ## Include Underscore templates
<%block name="header_extras"> <%block name="header_extras">
% for template_name in ["cohorts", "cohort-editor", "cohort-selector", "notification"]: % for template_name in ["cohorts", "cohort-editor", "cohort-selector", "add-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>
......
...@@ -220,31 +220,30 @@ ...@@ -220,31 +220,30 @@
</div> </div>
%if course.is_cohorted: %if course.is_cohorted:
<hr class="divider" /> <hr class="divider" />
<div class="cohort-management membership-section" <div class="cohort-management membership-section"
data-ajax_url="${section_data['cohorts_ajax_url']}" data-ajax_url="${section_data['cohorts_ajax_url']}"
data-advanced-settings-url="${section_data['advanced_settings_url']}" data-advanced-settings-url="${section_data['advanced_settings_url']}"
> >
</div> </div>
% endif
<%block name="headextra"> <%block name="headextra">
<script> <script>
$(document).ready(function() { $(document).ready(function() {
var cohortManagementElement = $('.cohort-management'); var cohortManagementElement = $('.cohort-management');
if (cohortManagementElement.length > 0) { if (cohortManagementElement.length > 0) {
var cohorts = new CohortCollection(); var cohorts = new CohortCollection();
cohorts.url = cohortManagementElement.data('ajax_url'); cohorts.url = cohortManagementElement.data('ajax_url');
var cohortsView = new CohortsView({ var cohortsView = new CohortsView({
el: cohortManagementElement, el: cohortManagementElement,
model: cohorts, model: cohorts,
advanced_settings_url: cohortManagementElement.data('advanced-settings-url') advanced_settings_url: cohortManagementElement.data('advanced-settings-url')
}); });
cohortsView.render(); cohorts.fetch().done(function() {
cohorts.fetch().done(function() { cohortsView.render();
cohortsView.render(); });
}); }
} });
}); </script>
</script> </%block>
</%block> % endif
...@@ -3,22 +3,29 @@ ...@@ -3,22 +3,29 @@
<%- title %> <%- title %>
</h3> </h3>
<% if (details.length > 0) { %> <% if (details.length > 0 || message) { %>
<div class="message-copy"> <div class="message-copy">
<ul class="list-summary summary-items"> <% if (message) { %>
<% for (var i = 0; i < details.length; i++) { %> <p><%- message %></p>
<li class="summary-item"> <% } %>
<%- details[i] %> <% if (details.length > 0) { %>
</li> <ul class="list-summary summary-items">
<% } %> <% for (var i = 0; i < details.length; i++) { %>
</ul> <li class="summary-item"><%- details[i] %></li>
<% } %>
</ul>
<% } %>
</div> </div>
<% } %> <% } %>
<% if (actionText) { %> <% if (actionText) { %>
<div class="message-actions"> <div class="message-actions">
<a href="" class="action-primary"><%- actionText %></a> <button class="action-primary <%- actionClass %>">
<% if (actionIconClass) { %>
<i class="icon <%- actionIconClass %>"></i>
<% } %>
<%- actionText %>
</button>
</div> </div>
<% } %> <% } %>
</div> </div>
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
<%block name="headextra"> <%block name="headextra">
<%static:css group='style-course-vendor'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/> <%static:css group='style-course'/>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
</%block> </%block>
<%block name="pagetitle">${_("{course_number} Staff Grading").format(course_number=course.display_number_with_default) | h}</%block> <%block name="pagetitle">${_("{course_number} Staff Grading").format(course_number=course.display_number_with_default) | h}</%block>
......
...@@ -19,7 +19,6 @@ ...@@ -19,7 +19,6 @@
window.Range.prototype = { }; window.Range.prototype = { };
} }
</script> </script>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/mustache.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/mustache.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.axislabels.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.axislabels.js')}"></script>
...@@ -244,9 +243,10 @@ ...@@ -244,9 +243,10 @@
<div class="form-fields"> <div class="form-fields">
<div class="cohort-management-nav-form-select field field-select"> <div class="cohort-management-nav-form-select field field-select">
<label for="cohort-select" class="label sr">Select a cohort to manage</label> <label for="cohort-select" class="label sr">Select a cohort group to manage</label>
<select class="input cohort-select" name="cohort-select" id="cohort-select"> <select class="input cohort-select" name="cohort-select" id="cohort-select">
<option>Select a cohort group</option>
<option value="cohort-name-1">Cohort Name is Placed Here and Should Accommodate Almost Everything (12,546)</option> <option value="cohort-name-1">Cohort Name is Placed Here and Should Accommodate Almost Everything (12,546)</option>
<option value="cohort-name-2" selected>Cras mattis consectetur purus sit amet fermentum (8,546)</option> <option value="cohort-name-2" selected>Cras mattis consectetur purus sit amet fermentum (8,546)</option>
<option value="cohort-name-3">Donec id elit non mi porta gravida at eget metus. (4) <option value="cohort-name-3">Donec id elit non mi porta gravida at eget metus. (4)
...@@ -310,7 +310,6 @@ ...@@ -310,7 +310,6 @@
<span class="sr">(Required Field)</span> <span class="sr">(Required Field)</span>
</label> </label>
<input type="text" name="cohort-create-name" value="" class=" input cohort-create-name" id="cohort-create-name" placeholder="Enter Your New Cohort Group's Name" required="required" /> <input type="text" name="cohort-create-name" value="" class=" input cohort-create-name" id="cohort-create-name" placeholder="Enter Your New Cohort Group's Name" required="required" />
<span class="tip">No special characters or symbols are allowed in group names</span>
</div> </div>
</div> </div>
...@@ -337,7 +336,6 @@ ...@@ -337,7 +336,6 @@
<span class="sr">(Required Field)</span> <span class="sr">(Required Field)</span>
</label> </label>
<input type="text" name="cohort-edit-name" value="" class=" input cohort-edit-name" id="cohort-edit-name" placeholder="Enter Your Cohort Group's Name" required="required" /> <input type="text" name="cohort-edit-name" value="" class=" input cohort-edit-name" id="cohort-edit-name" placeholder="Enter Your Cohort Group's Name" required="required" />
<span class="tip">No special characters or symbols are allowed in group names</span>
</div> </div>
</div> </div>
...@@ -356,10 +354,6 @@ ...@@ -356,10 +354,6 @@
<h3 class="message-title">New Cohort Name has been created. You can manually add students to this group below.</h3> <h3 class="message-title">New Cohort Name has been created. You can manually add students to this group below.</h3>
</div> </div>
<div class="message message-error">
<h3 class="message-title">Special characters are not allowed in cohort group names</h3>
</div>
<!-- individual group --> <!-- individual group -->
<div class="cohort-management-group"> <div class="cohort-management-group">
<header class="cohort-management-group-header"> <header class="cohort-management-group-header">
......
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