Commit 246e0e6f by Tim Babych

Merge pull request #4411 from edx/anton/group-experiments

Create new group configurations page
parents 851fe47a d30e986a
...@@ -13,7 +13,7 @@ from django.conf import settings ...@@ -13,7 +13,7 @@ from django.conf import settings
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponseBadRequest, HttpResponseNotFound, HttpResponse
from util.json_request import JsonResponse from util.json_request import JsonResponse
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
...@@ -21,6 +21,7 @@ from xmodule.error_module import ErrorDescriptor ...@@ -21,6 +21,7 @@ from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.tabs import PDFTextbookTabs from xmodule.tabs import PDFTextbookTabs
from xmodule.partitions.partitions import UserPartition, Group
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
...@@ -67,7 +68,7 @@ __all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler' ...@@ -67,7 +68,7 @@ __all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler'
'grading_handler', 'grading_handler',
'advanced_settings_handler', 'advanced_settings_handler',
'textbooks_list_handler', 'textbooks_detail_handler', 'textbooks_list_handler', 'textbooks_detail_handler',
'group_configurations_list_handler'] 'group_configurations_list_handler', 'group_configurations_detail_handler']
class AccessListFallback(Exception): class AccessListFallback(Exception):
...@@ -857,7 +858,56 @@ def textbooks_detail_handler(request, course_key_string, textbook_id): ...@@ -857,7 +858,56 @@ def textbooks_detail_handler(request, course_key_string, textbook_id):
return JsonResponse() return JsonResponse()
@require_http_methods(("GET")) class GroupConfigurationsValidationError(Exception):
"""
An error thrown when a group configurations input is invalid.
"""
pass
class GroupConfiguration(object):
"""
Prepare Group Configuration for the course.
"""
@staticmethod
def parse(configuration_json):
"""
Parse given string that represents group configuration.
"""
try:
group_configuration = json.loads(configuration_json)
except ValueError:
raise GroupConfigurationsValidationError(_("invalid JSON"))
if not group_configuration.get('version'):
group_configuration['version'] = UserPartition.VERSION
# this is temporary logic, we are going to build default groups on front-end
if not group_configuration.get('groups'):
group_configuration['groups'] = [
{'name': 'Group A'}, {'name': 'Group B'},
]
for group in group_configuration['groups']:
group['version'] = Group.VERSION
return group_configuration
@staticmethod
def validate(group_configuration):
"""
Validate group configuration representation.
"""
if not group_configuration.get("name"):
raise GroupConfigurationsValidationError(_("must have name of the configuration"))
if not isinstance(group_configuration.get("description"), basestring):
raise GroupConfigurationsValidationError(_("must have description of the configuration"))
if len(group_configuration.get('groups')) < 2:
raise GroupConfigurationsValidationError(_("must have at least two groups"))
group_id = unicode(group_configuration.get("id", ""))
if group_id and not group_id.isdigit():
raise GroupConfigurationsValidationError(_("group configuration ID must be numeric"))
@require_http_methods(("GET", "POST"))
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def group_configurations_list_handler(request, course_key_string): def group_configurations_list_handler(request, course_key_string):
...@@ -866,17 +916,56 @@ def group_configurations_list_handler(request, course_key_string): ...@@ -866,17 +916,56 @@ def group_configurations_list_handler(request, course_key_string):
GET GET
html: return Group Configurations list page (Backbone application) html: return Group Configurations list page (Backbone application)
POST
json: create new group configuration
""" """
course_key = CourseKey.from_string(course_key_string) course_key = CourseKey.from_string(course_key_string)
course = _get_course_module(course_key, request.user) course = _get_course_module(course_key, request.user)
store = modulestore()
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
group_configuration_url = reverse_course_url('group_configurations_list_handler', course_key) group_configuration_url = reverse_course_url('group_configurations_list_handler', course_key)
splite_test_enabled = SPLIT_TEST_COMPONENT_TYPE in course.advanced_modules split_test_enabled = SPLIT_TEST_COMPONENT_TYPE in course.advanced_modules
return render_to_response('group_configurations.html', { return render_to_response('group_configurations.html', {
'context_course': course, 'context_course': course,
'group_configuration_url': group_configuration_url, 'group_configuration_url': group_configuration_url,
'configurations': [u.to_json() for u in course.user_partitions] if splite_test_enabled else None, 'configurations': [u.to_json() for u in course.user_partitions] if split_test_enabled else None,
}) })
elif "application/json" in request.META.get('HTTP_ACCEPT') and request.method == 'POST':
# create a new group configuration for the course
try:
configuration = GroupConfiguration.parse(request.body)
GroupConfiguration.validate(configuration)
except GroupConfigurationsValidationError as err:
return JsonResponse({"error": err.message}, status=400)
if not configuration.get("id"):
configuration["id"] = random.randint(100, 10**12)
# Assign ids to every group in configuration.
for index, group in enumerate(configuration.get('groups', [])):
group["id"] = index
course.user_partitions.append(UserPartition.from_json(configuration))
store.update_item(course, request.user.id)
response = JsonResponse(configuration, status=201)
response["Location"] = reverse_course_url(
'group_configurations_detail_handler',
course.id,
kwargs={'group_configuration_id': configuration["id"]}
)
return response
else:
return HttpResponse(status=406)
@require_http_methods(("GET", "POST"))
@login_required
@ensure_csrf_cookie
def group_configurations_detail_handler(request, course_key_string, group_configuration_id):
return JsonResponse(status=404)
def _get_course_creator_status(user): def _get_course_creator_status(user):
......
import json
from unittest import skipUnless
from django.conf import settings
from contentstore.utils import reverse_course_url
from contentstore.tests.utils import CourseTestCase
@skipUnless(settings.FEATURES.get('ENABLE_GROUP_CONFIGURATIONS'), 'Tests Group Configurations feature')
class GroupConfigurationsCreateTestCase(CourseTestCase):
"""
Test cases for creating a new group configurations.
"""
def setUp(self):
"""
Set up a url and group configuration content for tests.
"""
super(GroupConfigurationsCreateTestCase, self).setUp()
self.url = reverse_course_url('group_configurations_list_handler', self.course.id)
self.group_configuration_json = {
u'description': u'Test description',
u'name': u'Test name'
}
def test_index_page(self):
"""
Check that the group configuration index page responds correctly.
"""
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertIn('New Group Configuration', response.content)
def test_group_success(self):
"""
Test that you can create a group configuration.
"""
expected_group_configuration = {
u'description': u'Test description',
u'name': u'Test name',
u'version': 1,
u'groups': [
{u'id': 0, u'name': u'Group A', u'version': 1},
{u'id': 1, u'name': u'Group B', u'version': 1}
]
}
response = self.client.post(
self.url,
data=json.dumps(self.group_configuration_json),
content_type="application/json",
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
)
self.assertEqual(response.status_code, 201)
self.assertIn("Location", response)
group_configuration = json.loads(response.content)
del group_configuration['id'] # do not check for id, it is unique
self.assertEqual(expected_group_configuration, group_configuration)
def test_bad_group(self):
"""
Test if only one group in configuration exist.
"""
# Only one group in group configuration here.
bad_group_configuration = {
u'description': u'Test description',
u'id': 1,
u'name': u'Test name',
u'version': 1,
u'groups': [
{u'id': 0, u'name': u'Group A', u'version': 1},
]
}
response = self.client.post(
self.url,
data=json.dumps(bad_group_configuration),
content_type="application/json",
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
)
self.assertEqual(response.status_code, 400)
self.assertNotIn("Location", response)
content = json.loads(response.content)
self.assertIn("error", content)
def test_bad_configuration_id(self):
"""
Test if configuration id is not numeric.
"""
# Configuration id is string here.
bad_group_configuration = {
u'description': u'Test description',
u'id': 'bad_id',
u'name': u'Test name',
u'version': 1,
u'groups': [
{u'id': 0, u'name': u'Group A', u'version': 1},
{u'id': 1, u'name': u'Group B', u'version': 1}
]
}
response = self.client.post(
self.url,
data=json.dumps(bad_group_configuration),
content_type="application/json",
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
)
self.assertEqual(response.status_code, 400)
self.assertNotIn("Location", response)
content = json.loads(response.content)
self.assertIn("error", content)
def test_bad_json(self):
"""
Test bad json handling.
"""
bad_jsons = [
{u'name': 'Test Name'},
{u'description': 'Test description'},
{}
]
for bad_json in bad_jsons:
response = self.client.post(
self.url,
data=json.dumps(bad_json),
content_type="application/json",
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(response.status_code, 400)
self.assertNotIn("Location", response)
content = json.loads(response.content)
self.assertIn("error", content)
def test_invalid_json(self):
"""
Test invalid json handling.
"""
# No property name.
invalid_json = "{u'name': 'Test Name', []}"
response = self.client.post(
self.url,
data=invalid_json,
content_type="application/json",
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(response.status_code, 400)
self.assertNotIn("Location", response)
content = json.loads(response.content)
self.assertIn("error", content)
define([ define([
'backbone', 'underscore', 'gettext', 'js/models/group', 'backbone', 'underscore', 'underscore.string', 'gettext', 'js/models/group',
'js/collections/group', 'backbone.associations', 'coffee/src/main' 'js/collections/group', 'backbone.associations', 'coffee/src/main'
], ],
function(Backbone, _, gettext, GroupModel, GroupCollection) { function(Backbone, _, str, gettext, GroupModel, GroupCollection) {
'use strict'; 'use strict';
_.str = str;
var GroupConfiguration = Backbone.AssociatedModel.extend({ var GroupConfiguration = Backbone.AssociatedModel.extend({
defaults: function() { defaults: function() {
return { return {
id: null,
name: '', name: '',
description: '', description: '',
groups: new GroupCollection([{}, {}]), groups: new GroupCollection([]),
showGroups: false showGroups: false,
editing: false
}; };
}, },
...@@ -55,32 +56,12 @@ function(Backbone, _, gettext, GroupModel, GroupCollection) { ...@@ -55,32 +56,12 @@ function(Backbone, _, gettext, GroupModel, GroupCollection) {
}, },
validate: function(attrs) { validate: function(attrs) {
if (!attrs.name) { if (!_.str.trim(attrs.name)) {
return { return {
message: gettext('Group Configuration name is required'), message: gettext('Group Configuration name is required'),
attributes: {name: true} attributes: {name: true}
}; };
} }
if (attrs.groups.length === 0) {
return {
message: gettext('Please add at least one group'),
attributes: {groups: true}
};
} else {
// validate all groups
var invalidGroups = [];
attrs.groups.each(function(group) {
if(!group.isValid()) {
invalidGroups.push(group);
}
});
if (!_.isEmpty(invalidGroups)) {
return {
message: gettext('All groups must have a name'),
attributes: {groups: invalidGroups}
};
}
}
} }
}); });
return GroupConfiguration; return GroupConfiguration;
......
define([ define([
'backbone', 'js/models/group_configuration', 'backbone', 'coffee/src/main', 'js/models/group_configuration',
'js/collections/group_configuration', 'js/models/group', 'js/models/group', 'js/collections/group'
'js/collections/group', 'coffee/src/main'
], function( ], function(
Backbone, GroupConfiguration, GroupConfigurationSet, Group, GroupSet, main Backbone, main, GroupConfigurationModel, GroupModel, GroupCollection
) { ) {
'use strict'; 'use strict';
beforeEach(function() { beforeEach(function() {
...@@ -14,10 +13,10 @@ define([ ...@@ -14,10 +13,10 @@ define([
}); });
}); });
describe('GroupConfiguration model', function() { describe('GroupConfigurationModel', function() {
beforeEach(function() { beforeEach(function() {
main(); main();
this.model = new GroupConfiguration(); this.model = new GroupConfigurationModel();
}); });
describe('Basic', function() { describe('Basic', function() {
...@@ -33,16 +32,10 @@ define([ ...@@ -33,16 +32,10 @@ define([
expect(this.model.get('showGroups')).toBeFalsy(); expect(this.model.get('showGroups')).toBeFalsy();
}); });
it('should have a GroupSet with two groups by default', function() { it('should be empty by default', function() {
var groups = this.model.get('groups'); var groups = this.model.get('groups');
expect(groups).toBeInstanceOf(GroupSet); expect(groups).toBeInstanceOf(GroupCollection);
expect(groups.length).toEqual(2);
expect(groups.at(0).isEmpty()).toBeTruthy();
expect(groups.at(1).isEmpty()).toBeTruthy();
});
it('should be empty by default', function() {
expect(this.model.isEmpty()).toBeTruthy(); expect(this.model.isEmpty()).toBeTruthy();
}); });
...@@ -53,23 +46,25 @@ define([ ...@@ -53,23 +46,25 @@ define([
expect(this.model.get('name')).toEqual(''); expect(this.model.get('name')).toEqual('');
}); });
it('should not be dirty by default', function() {
expect(this.model.isDirty()).toBeFalsy();
});
it('should be dirty after it\'s been changed', function() { it('should be dirty after it\'s been changed', function() {
this.model.set('name', 'foobar'); this.model.set('name', 'foobar');
expect(this.model.isDirty()).toBeTruthy(); expect(this.model.isDirty()).toBeTruthy();
}); });
it('should not be dirty after calling setOriginalAttributes', function() { describe('should not be dirty', function () {
it('by default', function() {
expect(this.model.isDirty()).toBeFalsy();
});
it('after calling setOriginalAttributes', function() {
this.model.set('name', 'foobar'); this.model.set('name', 'foobar');
this.model.setOriginalAttributes(); this.model.setOriginalAttributes();
expect(this.model.isDirty()).toBeFalsy(); expect(this.model.isDirty()).toBeFalsy();
}); });
}); });
});
describe('Input/Output', function() { describe('Input/Output', function() {
var deepAttributes = function(obj) { var deepAttributes = function(obj) {
...@@ -96,7 +91,7 @@ define([ ...@@ -96,7 +91,7 @@ define([
it('should match server model to client model', function() { it('should match server model to client model', function() {
var serverModelSpec = { var serverModelSpec = {
'id': 10, 'id': 10,
'name': 'My GroupConfiguration', 'name': 'My Group Configuration',
'description': 'Some description', 'description': 'Some description',
'groups': [ 'groups': [
{ {
...@@ -108,9 +103,10 @@ define([ ...@@ -108,9 +103,10 @@ define([
}, },
clientModelSpec = { clientModelSpec = {
'id': 10, 'id': 10,
'name': 'My GroupConfiguration', 'name': 'My Group Configuration',
'description': 'Some description', 'description': 'Some description',
'showGroups': false, 'showGroups': false,
'editing': false,
'groups': [ 'groups': [
{ {
'name': 'Group 1' 'name': 'Group 1'
...@@ -119,7 +115,7 @@ define([ ...@@ -119,7 +115,7 @@ define([
} }
] ]
}, },
model = new GroupConfiguration(serverModelSpec); model = new GroupConfigurationModel(serverModelSpec);
expect(deepAttributes(model)).toEqual(clientModelSpec); expect(deepAttributes(model)).toEqual(clientModelSpec);
expect(model.toJSON()).toEqual(serverModelSpec); expect(model.toJSON()).toEqual(serverModelSpec);
...@@ -128,55 +124,22 @@ define([ ...@@ -128,55 +124,22 @@ define([
describe('Validation', function() { describe('Validation', function() {
it('requires a name', function() { it('requires a name', function() {
var model = new GroupConfiguration({ name: '' }); var model = new GroupConfigurationModel({ name: '' });
expect(model.isValid()).toBeFalsy();
});
it('requires at least one group', function() {
var model = new GroupConfiguration({ name: 'foo' });
model.get('groups').reset();
expect(model.isValid()).toBeFalsy();
});
it('requires a valid group', function() {
var group = new Group(),
model = new GroupConfiguration({ name: 'foo' });
group.isValid = function() { return false; };
model.get('groups').reset([group]);
expect(model.isValid()).toBeFalsy();
});
it('requires all groups to be valid', function() {
var group1 = new Group(),
group2 = new Group(),
model = new GroupConfiguration({ name: 'foo' });
group1.isValid = function() { return true; };
group2.isValid = function() { return false; };
model.get('groups').reset([group1, group2]);
expect(model.isValid()).toBeFalsy(); expect(model.isValid()).toBeFalsy();
}); });
it('can pass validation', function() { it('can pass validation', function() {
var group = new Group(), var model = new GroupConfigurationModel({ name: 'foo' });
model = new GroupConfiguration({ name: 'foo' });
group.isValid = function() { return true; };
model.get('groups').reset([group]);
expect(model.isValid()).toBeTruthy(); expect(model.isValid()).toBeTruthy();
}); });
}); });
}); });
describe('Group model', function() { describe('GroupModel', function() {
beforeEach(function() { beforeEach(function() {
this.model = new Group(); this.model = new GroupModel();
}); });
describe('Basic', function() { describe('Basic', function() {
...@@ -191,22 +154,22 @@ define([ ...@@ -191,22 +154,22 @@ define([
describe('Validation', function() { describe('Validation', function() {
it('requires a name', function() { it('requires a name', function() {
var model = new Group({ name: '' }); var model = new GroupModel({ name: '' });
expect(model.isValid()).toBeFalsy(); expect(model.isValid()).toBeFalsy();
}); });
it('can pass validation', function() { it('can pass validation', function() {
var model = new Group({ name: 'a' }); var model = new GroupModel({ name: 'a' });
expect(model.isValid()).toBeTruthy(); expect(model.isValid()).toBeTruthy();
}); });
}); });
}); });
describe('Group collection', function() { describe('GroupCollection', function() {
beforeEach(function() { beforeEach(function() {
this.collection = new GroupSet(); this.collection = new GroupCollection();
}); });
it('is empty by default', function() { it('is empty by default', function() {
......
...@@ -9,19 +9,32 @@ define([ ...@@ -9,19 +9,32 @@ define([
), ),
noGroupConfigurationsTpl = readFixtures( noGroupConfigurationsTpl = readFixtures(
'no-group-configurations.underscore' 'no-group-configurations.underscore'
), view; ),
groupConfigurationEditTpl = readFixtures(
'group-configuration-edit.underscore'
);
var initializePage = function (disableSpy) { var initializePage = function (disableSpy) {
view = new GroupConfigurationsPage({ var view = new GroupConfigurationsPage({
el: $('.content-primary'), el: $('#content'),
collection: new GroupConfigurationCollection({ collection: new GroupConfigurationCollection({
name: 'Configuration 1' name: 'Configuration 1'
}) })
}); });
if (!disableSpy) { if (!disableSpy) {
spyOn(view, 'addGlobalActions'); spyOn(view, 'addWindowActions');
} }
return view;
};
var renderPage = function () {
return initializePage().render();
};
var clickNewConfiguration = function (view) {
view.$('.nav-actions .new-button').click();
}; };
beforeEach(function () { beforeEach(function () {
...@@ -29,12 +42,18 @@ define([ ...@@ -29,12 +42,18 @@ define([
id: 'no-group-configurations-tpl', id: 'no-group-configurations-tpl',
type: 'text/template' type: 'text/template'
}).text(noGroupConfigurationsTpl)); }).text(noGroupConfigurationsTpl));
appendSetFixtures($('<script>', {
id: 'group-configuration-edit-tpl',
type: 'text/template'
}).text(groupConfigurationEditTpl));
appendSetFixtures(mockGroupConfigurationsPage); appendSetFixtures(mockGroupConfigurationsPage);
}); });
describe('Initial display', function() { describe('Initial display', function() {
it('can render itself', function() { it('can render itself', function() {
initializePage(); var view = initializePage();
expect(view.$('.ui-loading')).toBeVisible(); expect(view.$('.ui-loading')).toBeVisible();
view.render(); view.render();
expect(view.$('.no-group-configurations-content')).toBeTruthy(); expect(view.$('.no-group-configurations-content')).toBeTruthy();
...@@ -45,9 +64,9 @@ define([ ...@@ -45,9 +64,9 @@ define([
describe('on page close/change', function() { describe('on page close/change', function() {
it('I see notification message if the model is changed', it('I see notification message if the model is changed',
function() { function() {
var message; var view = initializePage(true),
message;
initializePage(true);
view.render(); view.render();
message = view.onBeforeUnload(); message = view.onBeforeUnload();
expect(message).toBeUndefined(); expect(message).toBeUndefined();
...@@ -58,14 +77,21 @@ define([ ...@@ -58,14 +77,21 @@ define([
var expectedMessage = [ var expectedMessage = [
'You have unsaved changes. Do you really want to ', 'You have unsaved changes. Do you really want to ',
'leave this page?' 'leave this page?'
].join(''), message; ].join(''),
view = renderPage(),
message;
initializePage();
view.render();
view.collection.at(0).set('name', 'Configuration 2'); view.collection.at(0).set('name', 'Configuration 2');
message = view.onBeforeUnload(); message = view.onBeforeUnload();
expect(message).toBe(expectedMessage); expect(message).toBe(expectedMessage);
}); });
}); });
it('create new group configuration', function () {
var view = renderPage();
clickNewConfiguration(view);
expect($('.group-configuration-edit').length).toBeGreaterThan(0);
});
}); });
}); });
...@@ -4,13 +4,21 @@ define([ ...@@ -4,13 +4,21 @@ define([
function(BaseView, _, gettext) { function(BaseView, _, gettext) {
'use strict'; 'use strict';
var GroupConfigurationDetails = BaseView.extend({ var GroupConfigurationDetails = BaseView.extend({
tagName: 'section', tagName: 'div',
className: 'group-configuration',
events: { events: {
'click .show-groups': 'showGroups', 'click .show-groups': 'showGroups',
'click .hide-groups': 'hideGroups' 'click .hide-groups': 'hideGroups'
}, },
className: function () {
var index = this.model.collection.indexOf(this.model);
return [
'group-configuration-details',
'group-configuration-details-' + index
].join(' ');
},
initialize: function() { initialize: function() {
this.template = _.template( this.template = _.template(
$('#group-configuration-details-tpl').text() $('#group-configuration-details-tpl').text()
...@@ -41,7 +49,10 @@ function(BaseView, _, gettext) { ...@@ -41,7 +49,10 @@ function(BaseView, _, gettext) {
getGroupsCountTitle: function () { getGroupsCountTitle: function () {
var count = this.model.get('groups').length, var count = this.model.get('groups').length,
message = ngettext( message = ngettext(
// Translators: 'count' is number of groups that the group configuration contains. /*
Translators: 'count' is number of groups that the group
configuration contains.
*/
'Contains %(count)s group', 'Contains %(count)s groups', 'Contains %(count)s group', 'Contains %(count)s groups',
count count
); );
......
define([
'js/views/baseview', 'underscore', 'jquery', 'gettext'
],
function(BaseView, _, $, gettext) {
'use strict';
var GroupConfigurationEdit = BaseView.extend({
tagName: 'div',
events: {
'change .group-configuration-name-input': 'setName',
'change .group-configuration-description-input': 'setDescription',
'focus .input-text': 'onFocus',
'blur .input-text': 'onBlur',
'submit': 'setAndClose',
'click .action-cancel': 'cancel'
},
className: function () {
var index = this.model.collection.indexOf(this.model);
return [
'group-configuration-edit',
'group-configuration-edit-' + index
].join(' ');
},
initialize: function() {
this.template = this.loadTemplate('group-configuration-edit');
this.listenTo(this.model, 'invalid', this.render);
},
render: function() {
this.$el.html(this.template({
id: this.model.get('id'),
uniqueId: _.uniqueId(),
name: this.model.escape('name'),
description: this.model.escape('description'),
isNew: this.model.isNew(),
error: this.model.validationError
}));
return this;
},
setName: function(event) {
if(event && event.preventDefault) { event.preventDefault(); }
this.model.set(
'name', this.$('.group-configuration-name-input').val(),
{ silent: true }
);
},
setDescription: function(event) {
if(event && event.preventDefault) { event.preventDefault(); }
this.model.set(
'description',
this.$('.group-configuration-description-input').val(),
{ silent: true }
);
},
setValues: function() {
this.setName();
this.setDescription();
return this;
},
setAndClose: function(event) {
if(event && event.preventDefault) { event.preventDefault(); }
this.setValues();
if(!this.model.isValid()) {
return false;
}
this.runOperationShowingMessage(
gettext('Saving') + '&hellip;',
function () {
var dfd = $.Deferred();
this.model.save({}, {
success: function() {
this.model.setOriginalAttributes();
this.close();
dfd.resolve();
}.bind(this)
});
return dfd;
}.bind(this)
);
},
cancel: function(event) {
if(event && event.preventDefault) { event.preventDefault(); }
this.model.reset();
return this.close();
},
close: function() {
var groupConfigurations = this.model.collection;
this.remove();
if(this.model.isNew()) {
// if the group configuration has never been saved, remove it
groupConfigurations.remove(this.model);
} else {
// tell the model that it's no longer being edited
this.model.set('editing', false);
}
return this;
}
});
return GroupConfigurationEdit;
});
define([
'js/views/baseview', 'jquery', 'js/views/group_configuration_details',
'js/views/group_configuration_edit'
], function(
BaseView, $, GroupConfigurationDetails, GroupConfigurationEdit
) {
'use strict';
var GroupConfigurationsItem = BaseView.extend({
tagName: 'section',
attributes: {
'tabindex': -1
},
className: function () {
var index = this.model.collection.indexOf(this.model);
return [
'group-configuration',
'group-configurations-list-item',
'group-configurations-list-item-' + index
].join(' ');
},
initialize: function() {
this.listenTo(this.model, 'change:editing', this.render);
this.listenTo(this.model, 'remove', this.remove);
},
render: function() {
// Removes a view from the DOM, and calls stopListening to remove
// any bound events that the view has listened to.
if (this.view) {
this.view.remove();
}
if (this.model.get('editing')) {
this.view = new GroupConfigurationEdit({
model: this.model
});
} else {
this.view = new GroupConfigurationDetails({
model: this.model
});
}
this.$el.html(this.view.render().el);
return this;
}
});
return GroupConfigurationsItem;
});
define(['js/views/baseview', 'jquery', 'js/views/group_configuration_details'], define([
function(BaseView, $, GroupConfigurationDetailsView) { 'js/views/baseview', 'jquery', 'js/views/group_configuration_item'
], function(
BaseView, $, GroupConfigurationItemView
) {
'use strict'; 'use strict';
var GroupConfigurationsList = BaseView.extend({ var GroupConfigurationsList = BaseView.extend({
tagName: 'div', tagName: 'div',
className: 'group-configurations-list', className: 'group-configurations-list',
events: { }, events: {
'click .new-button': 'addOne'
},
initialize: function() { initialize: function() {
this.emptyTemplate = this.loadTemplate('no-group-configurations'); this.emptyTemplate = this.loadTemplate('no-group-configurations');
this.listenTo(this.collection, 'all', this.render); this.listenTo(this.collection, 'add', this.addNewItemView);
}, },
render: function() { render: function() {
var configurations = this.collection; var configurations = this.collection;
if(configurations.length === 0) { if(configurations.length === 0) {
this.$el.html(this.emptyTemplate()); this.$el.html(this.emptyTemplate());
} else { } else {
var frag = document.createDocumentFragment(); var frag = document.createDocumentFragment();
configurations.each(function(configuration) { configurations.each(function(configuration) {
var view = new GroupConfigurationDetailsView({ var view = new GroupConfigurationItemView({
model: configuration model: configuration
}); });
...@@ -29,6 +35,27 @@ function(BaseView, $, GroupConfigurationDetailsView) { ...@@ -29,6 +35,27 @@ function(BaseView, $, GroupConfigurationDetailsView) {
this.$el.html([frag]); this.$el.html([frag]);
} }
return this; return this;
},
addNewItemView: function (model) {
var view = new GroupConfigurationItemView({
model: model
});
// If items already exist, just append one new. Otherwise, overwrite
// no-content message.
if (this.collection.length > 1) {
this.$el.append(view.render().el);
} else {
this.$el.html(view.render().el);
}
view.$el.focus();
},
addOne: function(event) {
if(event && event.preventDefault) { event.preventDefault(); }
this.collection.add([{editing: true}]);
} }
}); });
......
...@@ -2,23 +2,30 @@ define([ ...@@ -2,23 +2,30 @@ define([
'jquery', 'underscore', 'gettext', 'js/views/baseview', 'jquery', 'underscore', 'gettext', 'js/views/baseview',
'js/views/group_configurations_list' 'js/views/group_configurations_list'
], ],
function ($, _, gettext, BaseView, ConfigurationsListView) { function ($, _, gettext, BaseView, GroupConfigurationsList) {
'use strict'; 'use strict';
var GroupConfigurationsPage = BaseView.extend({ var GroupConfigurationsPage = BaseView.extend({
initialize: function() { initialize: function() {
BaseView.prototype.initialize.call(this); BaseView.prototype.initialize.call(this);
this.listView = new ConfigurationsListView({ this.listView = new GroupConfigurationsList({
collection: this.collection collection: this.collection
}); });
}, },
render: function() { render: function() {
this.hideLoadingIndicator(); this.hideLoadingIndicator();
this.$el.append(this.listView.render().el); this.$('.content-primary').append(this.listView.render().el);
this.addGlobalActions(); this.addButtonActions();
this.addWindowActions();
}, },
addGlobalActions: function () { addButtonActions: function () {
this.$('.nav-actions .new-button').click(function (event) {
this.listView.addOne(event);
}.bind(this));
},
addWindowActions: function () {
$(window).on('beforeunload', this.onBeforeUnload.bind(this)); $(window).on('beforeunload', this.onBeforeUnload.bind(this));
}, },
......
...@@ -27,11 +27,21 @@ ...@@ -27,11 +27,21 @@
color: $gray; color: $gray;
} }
.new-button {
@include font-size(14);
margin-left: $baseline;
[class^="icon-"] {
margin-right: ($baseline/2);
}
}
.group-configuration { .group-configuration {
@extend %ui-window; @extend %ui-window;
position: relative; position: relative;
outline: none;
.view-group-configuration { .group-configuration-details {
padding: $baseline ($baseline*1.5); padding: $baseline ($baseline*1.5);
.group-configuration-header { .group-configuration-header {
...@@ -60,6 +70,7 @@ ...@@ -60,6 +70,7 @@
} }
.group-configuration-info { .group-configuration-info {
@extend %t-copy-sub1;
color: $gray-l1; color: $gray-l1;
margin-left: $baseline; margin-left: $baseline;
...@@ -152,6 +163,182 @@ ...@@ -152,6 +163,182 @@
opacity: 1.0; opacity: 1.0;
} }
} }
.group-configuration-edit {
@include box-sizing(border-box);
border-radius: 2px;
width: 100%;
background: $white;
.wrapper-form {
padding: $baseline ($baseline*1.5);
}
.tip {
@extend %t-copy-sub2;
@include transition(color, 0.15s, ease-in-out);
display: block;
margin-top: ($baseline/4);
color: $gray-l3;
}
.is-focused .tip{
color: $gray;
}
.actions {
box-shadow: inset 0 1px 2px $shadow;
border-top: 1px solid $gray-l1;
padding: ($baseline*0.75) $baseline;
background: $gray-l6;
.action {
margin-right: ($baseline/4);
&:last-child {
margin-right: 0;
}
}
// add a group is below with groups styling
.action-primary {
@include blue-button;
@extend %t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
font-weight: 600;
text-transform: uppercase;
}
.action-secondary {
@include grey-button;
@extend %t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
font-weight: 600;
text-transform: uppercase;
}
}
.copy {
@extend %t-copy-sub2;
margin: ($baseline) 0 ($baseline/2) 0;
color: $gray;
strong {
font-weight: 600;
}
}
.group-configuration-fields {
@extend %cont-no-list;
.field {
margin: 0 0 ($baseline*0.75) 0;
&:last-child {
margin-bottom: 0;
}
&.required {
label {
font-weight: 600;
}
label:after {
margin-left: ($baseline/4);
content: "*";
}
}
label, input, textarea {
display: block;
}
textarea {
resize: vertical;
}
label {
@extend %t-copy-sub1;
@include transition(color, 0.15s, ease-in-out);
margin: 0 0 ($baseline/4) 0;
&.is-focused {
color: $blue;
}
}
//this section is borrowed from _account.scss - we should clean up and unify later
input, textarea {
@extend %t-copy-base;
height: 100%;
width: 100%;
padding: ($baseline/2);
&.long {
width: 100%;
}
&.short {
width: 25%;
}
::-webkit-input-placeholder {
color: $gray-l4;
}
:-moz-placeholder {
color: $gray-l3;
}
::-moz-placeholder {
color: $gray-l3;
}
:-ms-input-placeholder {
color: $gray-l3;
}
&:focus {
+ .tip {
color: $gray;
}
}
}
&.error {
label {
color: $red;
}
input {
border-color: $red;
}
}
&.add-group-configuration-name label {
@extend %t-title5;
}
}
label.required {
font-weight: 600;
&:after {
margin-left: ($baseline/4);
content: "*";
}
}
}
.group-configuration-fields {
margin-bottom: $baseline;
}
}
} }
.content-supplementary { .content-supplementary {
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<%block name="bodyclass">is-signedin course view-group-configurations</%block> <%block name="bodyclass">is-signedin course view-group-configurations</%block>
<%block name="header_extras"> <%block name="header_extras">
% for template_name in ["group-configuration-details", "no-group-configurations", "basic-modal", "modal-button"]: % for template_name in ["group-configuration-details", "group-configuration-edit", "no-group-configurations", "basic-modal", "modal-button"]:
<script type="text/template" id="${template_name}-tpl"> <script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" /> <%static:include path="js/${template_name}.underscore" />
</script> </script>
...@@ -20,14 +20,16 @@ ...@@ -20,14 +20,16 @@
<%block name="jsextra"> <%block name="jsextra">
<script type="text/javascript"> <script type="text/javascript">
require(["!domReady", "js/collections/group_configuration", "js/views/pages/group_configurations"], require(["domReady!", "js/collections/group_configuration", "js/views/pages/group_configurations"],
function(doc, GroupConfigurationCollection, GroupConfigurationsPage, xmoduleLoader) { function(doc, GroupConfigurationCollection, GroupConfigurationsPage) {
% if configurations is not None: % if configurations is not None:
var view = new GroupConfigurationsPage({ var collection = new GroupConfigurationCollection(${json.dumps(configurations)});
el: $('.content-primary'),
collection: new GroupConfigurationCollection(${json.dumps(configurations)}, { url: "${group_configuration_url}" }) collection.url = "${group_configuration_url}";
}); new GroupConfigurationsPage({
view.render(); el: $('#content'),
collection: collection
}).render();
% endif % endif
}); });
</script> </script>
...@@ -40,6 +42,14 @@ function(doc, GroupConfigurationCollection, GroupConfigurationsPage, xmoduleLoad ...@@ -40,6 +42,14 @@ function(doc, GroupConfigurationCollection, GroupConfigurationsPage, xmoduleLoad
<small class="subtitle">${_("Settings")}</small> <small class="subtitle">${_("Settings")}</small>
<span class="sr">&gt; </span>${_("Group Configurations")} <span class="sr">&gt; </span>${_("Group Configurations")}
</h1> </h1>
<nav class="nav-actions">
<h3 class="sr">${_("Page Actions")}</h3>
<ul>
<li class="nav-item">
<a href="#" class="button new-button"><i class="icon-plus"></i> ${_("New Group Configuration")}</a>
</li>
</ul>
</nav>
</header> </header>
</div> </div>
......
<div class="view-group-configuration view-group-configuration-<%= index %>"> <div class="wrapper-group-configuration">
<div class="wrapper-group-configuration">
<header class="group-configuration-header"> <header class="group-configuration-header">
<h3 class="group-configuration-title"> <h3 class="group-configuration-title">
<a href="#" class="group-toggle <% if(showGroups){ print('hide'); } else { print('show'); } %>-groups"> <a href="#" class="group-toggle <% if(showGroups){ print('hide'); } else { print('show'); } %>-groups">
...@@ -10,7 +9,7 @@ ...@@ -10,7 +9,7 @@
</header> </header>
<ol class="group-configuration-info group-configuration-info-<% if(showGroups){ print('block'); } else { print('inline'); } %>"> <ol class="group-configuration-info group-configuration-info-<% if(showGroups){ print('block'); } else { print('inline'); } %>">
<% if (_.isNumber(id)) { %> <% if (!_.isUndefined(id)) { %>
<li class="group-configuration-id" <li class="group-configuration-id"
><span class="group-configuration-label"><%= gettext('ID') %>: </span ><span class="group-configuration-label"><%= gettext('ID') %>: </span
><span class="group-configuration-value"><%= id %></span ><span class="group-configuration-value"><%= id %></span
...@@ -38,5 +37,4 @@ ...@@ -38,5 +37,4 @@
<% }) %> <% }) %>
</ol> </ol>
<% } %> <% } %>
</div>
</div> </div>
<form class="group-configuration-edit-form">
<div class="wrapper-form">
<% if (error && error.message) { %>
<div class="group-configuration-edit-error message message-status message-status error is-shown" name="group-configuration-edit-error">
<%= gettext(error.message) %>
</div>
<% } %>
<fieldset class="group-configuration-fields">
<legend class="sr"><%= gettext("Group Configuration information") %></legend>
<div class="input-wrap field text required add-group-configuration-name <% if(error && error.attributes && error.attributes.name) { print('error'); } %>">
<label for="group-configuration-name-<%= uniqueId %>"><%= gettext("Group Configuration Name") %></label>
<input id="group-configuration-name-<%= uniqueId %>" class="group-configuration-name-input input-text" name="group-configuration-name" type="text" placeholder="<%= gettext("This is the Name of the Group Configuration") %>" value="<%= name %>">
<span class="tip tip-stacked"><%= gettext("Name or short description of the configuration") %></span>
</div>
<div class="input-wrap field text add-group-configuration-description">
<label for="group-configuration-description-<%= uniqueId %>"><%= gettext("Description") %></label>
<textarea id="group-configuration-description-<%= uniqueId %>" class="group-configuration-description-input text input-text" name="group-configuration-description" placeholder="<%= gettext("This is the Description of the Group Configuration") %>"><%= description %></textarea>
<span class="tip tip-stacked"><%= gettext("Optional long description") %></span>
</div>
</fieldset>
</div>
<div class="actions">
<button class="action action-primary" type="submit"><% if (isNew) { print(gettext("Create")) } else { print(gettext("Save")) } %></button>
<button class="action action-secondary action-cancel"><%= gettext("Cancel") %></button>
</div>
</form>
<div id="content"> <div id="content">
<div class="wrapper-mast wrapper"> <div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle"> <header class="mast has-actions has-subtitle">
<h1 class="page-header"> <h1 class="page-header">
<small class="subtitle">${_("Settings")}</small> <small class="subtitle">${_("Settings")}</small>
<span class="sr">&gt; </span>${_("Group Configurations")} <span class="sr">&gt; </span>${_("Group Configurations")}
</h1> </h1>
<nav class="nav-actions">
<h3 class="sr">${_("Page Actions")}</h3>
<ul>
<li class="nav-item">
<a href="#" class="button new-button"><i class="icon-plus"></i> ${_("New Group Configuration")}</a>
</li>
</ul>
</nav>
</header> </header>
</div> </div>
......
<div class="no-group-configurations-content"> <div class="no-group-configurations-content">
<p><%= gettext("You haven't created any group configurations yet.") %></p> <p><%= gettext("You haven't created any group configurations yet.") %><a href="#" class="button new-button"><i class="icon-plus"></i><%= gettext("Add your first Group Configuration") %></a></p>
</div> </div>
...@@ -93,8 +93,11 @@ urlpatterns += patterns( ...@@ -93,8 +93,11 @@ urlpatterns += patterns(
) )
if settings.FEATURES.get('ENABLE_GROUP_CONFIGURATIONS'): if settings.FEATURES.get('ENABLE_GROUP_CONFIGURATIONS'):
urlpatterns += (url(r'^group_configurations/(?P<course_key_string>[^/]+)$', urlpatterns += patterns('contentstore.views',
'contentstore.views.group_configurations_list_handler'),) url(r'^group_configurations/(?P<course_key_string>[^/]+)$', 'group_configurations_list_handler'),
url(r'^group_configurations/(?P<course_key_string>[^/]+)/(?P<group_configuration_id>\d+)/?$',
'group_configurations_detail_handler'),
)
js_info_dict = { js_info_dict = {
'domain': 'djangojs', 'domain': 'djangojs',
......
...@@ -19,9 +19,15 @@ class GroupConfigurationsPage(CoursePage): ...@@ -19,9 +19,15 @@ class GroupConfigurationsPage(CoursePage):
""" """
Returns list of the group configurations for the course. Returns list of the group configurations for the course.
""" """
css = '.wrapper-group-configuration' css = '.group-configurations-list-item'
return [GroupConfiguration(self, index) for index in xrange(len(self.q(css=css)))] return [GroupConfiguration(self, index) for index in xrange(len(self.q(css=css)))]
def create(self):
"""
Creates new group configuration.
"""
self.q(css=".new-button").first.click()
class GroupConfiguration(object): class GroupConfiguration(object):
""" """
...@@ -30,7 +36,7 @@ class GroupConfiguration(object): ...@@ -30,7 +36,7 @@ class GroupConfiguration(object):
def __init__(self, page, index): def __init__(self, page, index):
self.page = page self.page = page
self.SELECTOR = '.view-group-configuration-{}'.format(index) self.SELECTOR = '.group-configurations-list-item-{}'.format(index)
self.index = index self.index = index
def get_selector(self, css=''): def get_selector(self, css=''):
...@@ -49,6 +55,31 @@ class GroupConfiguration(object): ...@@ -49,6 +55,31 @@ class GroupConfiguration(object):
css = 'a.group-toggle' css = 'a.group-toggle'
self.find_css(css).first.click() self.find_css(css).first.click()
def save(self):
"""
Save group configuration.
"""
css = '.action-primary'
self.find_css(css).first.click()
self.page.wait_for_ajax()
def cancel(self):
"""
Cancel group configuration.
"""
css = '.action-secondary'
self.find_css(css).first.click()
@property
def mode(self):
"""
Returns group configuration mode.
"""
if self.find_css('.group-configuration-edit').present:
return 'edit'
elif self.find_css('.group-configuration-details').present:
return 'details'
@property @property
def id(self): def id(self):
""" """
...@@ -58,6 +89,14 @@ class GroupConfiguration(object): ...@@ -58,6 +89,14 @@ class GroupConfiguration(object):
return self.find_css(css).first.text[0] return self.find_css(css).first.text[0]
@property @property
def validation_message(self):
"""
Returns validation message.
"""
css = '.message-status.error'
return self.find_css(css).first.text[0]
@property
def name(self): def name(self):
""" """
Returns group configuration name. Returns group configuration name.
...@@ -65,6 +104,14 @@ class GroupConfiguration(object): ...@@ -65,6 +104,14 @@ class GroupConfiguration(object):
css = '.group-configuration-title' css = '.group-configuration-title'
return self.find_css(css).first.text[0] return self.find_css(css).first.text[0]
@name.setter
def name(self, value):
"""
Sets group configuration name.
"""
css = '.group-configuration-name-input'
self.find_css(css).first.fill(value)
@property @property
def description(self): def description(self):
""" """
...@@ -73,6 +120,14 @@ class GroupConfiguration(object): ...@@ -73,6 +120,14 @@ class GroupConfiguration(object):
css = '.group-configuration-description' css = '.group-configuration-description'
return self.find_css(css).first.text[0] return self.find_css(css).first.text[0]
@description.setter
def description(self, value):
"""
Sets group configuration description.
"""
css = '.group-configuration-description-input'
self.find_css(css).first.fill(value)
@property @property
def groups(self): def groups(self):
""" """
......
...@@ -11,7 +11,7 @@ from ..pages.studio.component_editor import ComponentEditorView ...@@ -11,7 +11,7 @@ from ..pages.studio.component_editor import ComponentEditorView
from ..pages.studio.utils import add_discussion from ..pages.studio.utils import add_discussion
from unittest import skip from unittest import skip
from bok_choy.promise import Promise
class ContainerBase(UniqueCourseTest): class ContainerBase(UniqueCourseTest):
""" """
...@@ -93,6 +93,30 @@ class ContainerBase(UniqueCourseTest): ...@@ -93,6 +93,30 @@ class ContainerBase(UniqueCourseTest):
break break
self.assertEqual(len(blocks_checked), len(xblocks)) self.assertEqual(len(blocks_checked), len(xblocks))
def verify_groups(self, container, active_groups, inactive_groups):
"""
Check that the groups appear and are correctly categorized as to active and inactive.
Also checks that the "add missing groups" button/link is not present unless a value of False is passed
for verify_missing_groups_not_present.
"""
def wait_for_xblocks_to_render():
# First xblock is the container for the page, subtract 1.
return (len(active_groups) + len(inactive_groups) == len(container.xblocks) - 1, len(active_groups))
Promise(wait_for_xblocks_to_render, "Number of xblocks on the page are incorrect").fulfill()
def check_xblock_names(expected_groups, actual_blocks):
self.assertEqual(len(expected_groups), len(actual_blocks))
for idx, expected in enumerate(expected_groups):
self.assertEqual('Expand or Collapse\n{}'.format(expected), actual_blocks[idx].name)
check_xblock_names(active_groups, container.active_xblocks)
check_xblock_names(inactive_groups, container.inactive_xblocks)
# Verify inactive xblocks appear after active xblocks
check_xblock_names(active_groups + inactive_groups, container.xblocks[1:])
def do_action_and_verify(self, action, expected_ordering): def do_action_and_verify(self, action, expected_ordering):
""" """
Perform the supplied action and then verify the resulting ordering. Perform the supplied action and then verify the resulting ordering.
......
...@@ -58,29 +58,7 @@ class SplitTest(ContainerBase): ...@@ -58,29 +58,7 @@ class SplitTest(ContainerBase):
self.user = course_fix.user self.user = course_fix.user
def verify_groups(self, container, active_groups, inactive_groups, verify_missing_groups_not_present=True): def verify_groups(self, container, active_groups, inactive_groups, verify_missing_groups_not_present=True):
""" super(SplitTest, self).verify_groups(container, active_groups, inactive_groups)
Check that the groups appear and are correctly categorized as to active and inactive.
Also checks that the "add missing groups" button/link is not present unless a value of False is passed
for verify_missing_groups_not_present.
"""
def wait_for_xblocks_to_render():
# First xblock is the container for the page, subtract 1.
return (len(active_groups) + len(inactive_groups) == len(container.xblocks) - 1, len(active_groups))
Promise(wait_for_xblocks_to_render, "Number of xblocks on the page are incorrect").fulfill()
def check_xblock_names(expected_groups, actual_blocks):
self.assertEqual(len(expected_groups), len(actual_blocks))
for idx, expected in enumerate(expected_groups):
self.assertEqual('Expand or Collapse\n{}'.format(expected), actual_blocks[idx].name)
check_xblock_names(active_groups, container.active_xblocks)
check_xblock_names(inactive_groups, container.inactive_xblocks)
# Verify inactive xblocks appear after active xblocks
check_xblock_names(active_groups + inactive_groups, container.xblocks[1:])
if verify_missing_groups_not_present: if verify_missing_groups_not_present:
self.verify_add_missing_groups_button_not_present(container) self.verify_add_missing_groups_button_not_present(container)
...@@ -231,33 +209,33 @@ class SettingsMenuTest(UniqueCourseTest): ...@@ -231,33 +209,33 @@ class SettingsMenuTest(UniqueCourseTest):
@skipUnless(os.environ.get('FEATURE_GROUP_CONFIGURATIONS'), 'Tests Group Configurations feature') @skipUnless(os.environ.get('FEATURE_GROUP_CONFIGURATIONS'), 'Tests Group Configurations feature')
class GroupConfigurationsTest(UniqueCourseTest): class GroupConfigurationsTest(ContainerBase):
""" """
Tests that Group Configurations page works correctly with previously Tests that Group Configurations page works correctly with previously
added configurations in Studio added configurations in Studio
""" """
__test__ = True
def setUp(self): def setup_fixtures(self):
super(GroupConfigurationsTest, self).setUp()
course_fix = CourseFixture(**self.course_info) course_fix = CourseFixture(**self.course_info)
course_fix.add_advanced_settings({ course_fix.add_advanced_settings({
u"advanced_modules": {"value": ["split_test"]}, u"advanced_modules": {"value": ["split_test"]},
}) })
course_fix.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
XBlockFixtureDesc('vertical', 'Test Unit')
)
)
).install()
course_fix.install()
self.course_fix = course_fix self.course_fix = course_fix
self.user = course_fix.user
self.auth_page = AutoAuthPage( self.course_fix = course_fix
self.browser, self.user = course_fix.user
staff=False,
username=course_fix.user.get('username'),
email=course_fix.user.get('email'),
password=course_fix.user.get('password')
)
self.auth_page.visit()
def setUp(self):
super(GroupConfigurationsTest, self).setUp()
self.page = GroupConfigurationsPage( self.page = GroupConfigurationsPage(
self.browser, self.browser,
self.course_info['org'], self.course_info['org'],
...@@ -298,6 +276,7 @@ class GroupConfigurationsTest(UniqueCourseTest): ...@@ -298,6 +276,7 @@ class GroupConfigurationsTest(UniqueCourseTest):
config = self.page.group_configurations()[0] config = self.page.group_configurations()[0]
self.assertIn("Name of the Group Configuration", config.name) self.assertIn("Name of the Group Configuration", config.name)
self.assertEqual(config.id, '0') self.assertEqual(config.id, '0')
# Expand the configuration
config.toggle() config.toggle()
self.assertIn("Description of the group configuration.", config.description) self.assertIn("Description of the group configuration.", config.description)
self.assertEqual(len(config.groups), 2) self.assertEqual(len(config.groups), 2)
...@@ -308,8 +287,111 @@ class GroupConfigurationsTest(UniqueCourseTest): ...@@ -308,8 +287,111 @@ class GroupConfigurationsTest(UniqueCourseTest):
config = self.page.group_configurations()[1] config = self.page.group_configurations()[1]
self.assertIn("Name of second Group Configuration", config.name) self.assertIn("Name of second Group Configuration", config.name)
self.assertEqual(len(config.groups), 0) # no groups when the partition is collapsed self.assertEqual(len(config.groups), 0) # no groups when the partition is collapsed
# Expand the configuration
config.toggle() config.toggle()
self.assertEqual(len(config.groups), 3) self.assertEqual(len(config.groups), 3)
self.assertEqual("Beta", config.groups[1].name) self.assertEqual("Beta", config.groups[1].name)
self.assertEqual("33%", config.groups[1].allocation) self.assertEqual("33%", config.groups[1].allocation)
def test_can_create_group_configuration(self):
"""
Ensure that the group configuration can be created correctly.
"""
self.page.visit()
self.assertEqual(len(self.page.group_configurations()), 0)
# Create new group configuration
self.page.create()
config = self.page.group_configurations()[0]
config.name = "New Group Configuration Name"
config.description = "New Description of the group configuration."
# Save the configuration
config.save()
self.assertEqual(config.mode, 'details')
self.assertIn("New Group Configuration Name", config.name)
self.assertTrue(config.id)
# Expand the configuration
config.toggle()
self.assertIn("New Description of the group configuration.", config.description)
self.assertEqual(len(config.groups), 2)
self.assertEqual("Group A", config.groups[0].name)
self.assertEqual("Group B", config.groups[1].name)
self.assertEqual("50%", config.groups[0].allocation)
def test_use_group_configuration(self):
"""
Create and use group configuration
"""
self.page.visit()
self.assertEqual(len(self.page.group_configurations()), 0)
# Create new group configuration
self.page.create()
config = self.page.group_configurations()[0]
config.name = "New Group Configuration Name"
config.description = "New Description of the group configuration."
# Save the configuration
config.save()
unit = self.go_to_unit_page(make_draft=True)
add_advanced_component(unit, 0, 'split_test')
container = self.go_to_container_page()
container.edit()
component_editor = ComponentEditorView(self.browser, container.locator)
component_editor.set_select_value_and_save('Group Configuration', 'New Group Configuration Name')
self.verify_groups(container, ['Group A', 'Group B'], [])
def test_can_cancel_creation_of_group_configuration(self):
"""
Ensure that creation of the group configuration can be canceled correctly.
"""
self.page.visit()
self.assertEqual(len(self.page.group_configurations()), 0)
# Create new group configuration
self.page.create()
config = self.page.group_configurations()[0]
config.name = "Name of the Group Configuration"
config.description = "Description of the group configuration."
# Cancel the configuration
config.cancel()
self.assertEqual(len(self.page.group_configurations()), 0)
def test_group_configuration_validation(self):
"""
Ensure that validation of the group configuration works correctly.
"""
self.page.visit()
# Create new group configuration
self.page.create()
# Leave empty required field
config = self.page.group_configurations()[0]
config.description = "Description of the group configuration."
# Try to save
config.save()
# Verify that configuration is still in editing mode
self.assertEqual(config.mode, 'edit')
# Verify error message
self.assertEqual(
"Group Configuration name is required",
config.validation_message
)
# Set required field
config.name = "Name of the Group Configuration"
# Save the configuration
config.save()
# Verify the configuration is saved and it is shown in `details` mode.
self.assertEqual(config.mode, 'details')
# Verify the configuration for the data correctness
self.assertIn("Name of the Group Configuration", config.name)
self.assertTrue(config.id)
# Expand the configuration
config.toggle()
self.assertIn("Description of the group configuration.", config.description)
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