BLD-1110: Create, edit, delete groups.
This commit is contained in:
@@ -901,12 +901,11 @@ class GroupConfiguration(object):
|
||||
if len(self.configuration.get('groups', [])) < 2:
|
||||
raise GroupConfigurationsValidationError(_("must have at least two groups"))
|
||||
|
||||
def generate_id(self):
|
||||
def generate_id(self, used_ids):
|
||||
"""
|
||||
Generate unique id for the group configuration.
|
||||
If this id is already used, we generate new one.
|
||||
"""
|
||||
used_ids = self.get_used_ids()
|
||||
cid = random.randint(100, 10 ** 12)
|
||||
|
||||
while cid in used_ids:
|
||||
@@ -918,21 +917,18 @@ class GroupConfiguration(object):
|
||||
"""
|
||||
Assign id for the json representation of group configuration.
|
||||
"""
|
||||
self.configuration['id'] = int(configuration_id) if configuration_id else self.generate_id()
|
||||
self.configuration['id'] = int(configuration_id) if configuration_id else self.generate_id(self.get_used_ids())
|
||||
|
||||
def assign_group_ids(self):
|
||||
"""
|
||||
Assign ids for the group_configuration's groups.
|
||||
"""
|
||||
# this is temporary logic, we are going to build default groups on front-end
|
||||
if not self.configuration.get('groups'):
|
||||
self.configuration['groups'] = [
|
||||
{'name': 'Group A'}, {'name': 'Group B'},
|
||||
]
|
||||
|
||||
used_ids = [g.id for p in self.course.user_partitions for g in p.groups]
|
||||
# Assign ids to every group in configuration.
|
||||
for index, group in enumerate(self.configuration.get('groups', [])):
|
||||
group['id'] = index
|
||||
for group in self.configuration.get('groups', []):
|
||||
if group.get('id') is None:
|
||||
group["id"] = self.generate_id(used_ids)
|
||||
used_ids.append(group["id"])
|
||||
|
||||
def get_used_ids(self):
|
||||
"""
|
||||
|
||||
@@ -5,6 +5,7 @@ import json
|
||||
from unittest import skipUnless
|
||||
from django.conf import settings
|
||||
from contentstore.utils import reverse_course_url
|
||||
from contentstore.views.component import SPLIT_TEST_COMPONENT_TYPE
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from xmodule.partitions.partitions import Group, UserPartition
|
||||
|
||||
@@ -12,6 +13,10 @@ from xmodule.partitions.partitions import Group, UserPartition
|
||||
GROUP_CONFIGURATION_JSON = {
|
||||
u'name': u'Test name',
|
||||
u'description': u'Test description',
|
||||
u'groups': [
|
||||
{u'name': u'Group A'},
|
||||
{u'name': u'Group B'},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +101,6 @@ class GroupConfigurationsListHandlerTestCase(CourseTestCase, GroupConfigurations
|
||||
"""
|
||||
Test cases for group_configurations_list_handler.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up GroupConfigurationsListHandlerTestCase.
|
||||
@@ -109,13 +113,35 @@ class GroupConfigurationsListHandlerTestCase(CourseTestCase, GroupConfigurations
|
||||
"""
|
||||
return reverse_course_url('group_configurations_list_handler', self.course.id)
|
||||
|
||||
def test_can_retrieve_html(self):
|
||||
def test_view_index_ok(self):
|
||||
"""
|
||||
Check that the group configuration index page responds correctly.
|
||||
Basic check that the groups configuration page responds correctly.
|
||||
"""
|
||||
|
||||
self.course.user_partitions = [
|
||||
UserPartition(0, 'First name', 'First description', [Group(0, 'Group A'), Group(1, 'Group B'), Group(2, 'Group C')]),
|
||||
]
|
||||
self.save_course()
|
||||
|
||||
if SPLIT_TEST_COMPONENT_TYPE not in self.course.advanced_modules:
|
||||
self.course.advanced_modules.append(SPLIT_TEST_COMPONENT_TYPE)
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
|
||||
response = self.client.get(self._url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('New Group Configuration', response.content)
|
||||
self.assertContains(response, 'First name')
|
||||
self.assertContains(response, 'Group C')
|
||||
|
||||
def test_view_index_disabled(self):
|
||||
"""
|
||||
Check that group configuration page is not displayed when turned off.
|
||||
"""
|
||||
if SPLIT_TEST_COMPONENT_TYPE in self.course.advanced_modules:
|
||||
self.course.advanced_modules.remove(SPLIT_TEST_COMPONENT_TYPE)
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
|
||||
resp = self.client.get(self._url())
|
||||
self.assertContains(resp, "module is disabled")
|
||||
|
||||
def test_unsupported_http_accept_header(self):
|
||||
"""
|
||||
@@ -157,8 +183,12 @@ class GroupConfigurationsListHandlerTestCase(CourseTestCase, GroupConfigurations
|
||||
self.assertEqual(len(group_ids), 2)
|
||||
self.reload_course()
|
||||
# Verify that user_partitions in the course contains the new group configuration.
|
||||
self.assertEqual(len(self.course.user_partitions), 1)
|
||||
self.assertEqual(self.course.user_partitions[0].name, u'Test name')
|
||||
user_partititons = self.course.user_partitions
|
||||
self.assertEqual(len(user_partititons), 1)
|
||||
self.assertEqual(user_partititons[0].name, u'Test name')
|
||||
self.assertEqual(len(user_partititons[0].groups), 2)
|
||||
self.assertEqual(user_partititons[0].groups[0].name, u'Group A')
|
||||
self.assertEqual(user_partititons[0].groups[1].name, u'Group B')
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
@@ -204,7 +234,7 @@ class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfiguratio
|
||||
|
||||
response = self.client.put(
|
||||
self._url(cid=999),
|
||||
data=json.dumps(GROUP_CONFIGURATION_JSON),
|
||||
data=json.dumps(expected),
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
|
||||
@@ -213,8 +243,12 @@ class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfiguratio
|
||||
self.assertEqual(content, expected)
|
||||
self.reload_course()
|
||||
# Verify that user_partitions in the course contains the new group configuration.
|
||||
self.assertEqual(len(self.course.user_partitions), 1)
|
||||
self.assertEqual(self.course.user_partitions[0].name, u'Test name')
|
||||
user_partititons = self.course.user_partitions
|
||||
self.assertEqual(len(user_partititons), 1)
|
||||
self.assertEqual(user_partititons[0].name, u'Test name')
|
||||
self.assertEqual(len(user_partititons[0].groups), 2)
|
||||
self.assertEqual(user_partititons[0].groups[0].name, u'Group A')
|
||||
self.assertEqual(user_partititons[0].groups[1].name, u'Group B')
|
||||
|
||||
def test_can_edit_group_configuration(self):
|
||||
"""
|
||||
@@ -231,8 +265,8 @@ class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfiguratio
|
||||
u'description': u'New Test description',
|
||||
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},
|
||||
{u'id': 0, u'name': u'New Group Name', u'version': 1},
|
||||
{u'id': 2, u'name': u'Group C', u'version': 1},
|
||||
],
|
||||
}
|
||||
response = self.client.put(
|
||||
@@ -246,5 +280,9 @@ class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfiguratio
|
||||
self.assertEqual(content, expected)
|
||||
self.reload_course()
|
||||
# Verify that user_partitions is properly updated in the course.
|
||||
self.assertEqual(len(self.course.user_partitions), 1)
|
||||
self.assertEqual(self.course.user_partitions[0].name, u'New Test name')
|
||||
user_partititons = self.course.user_partitions
|
||||
self.assertEqual(len(user_partititons), 1)
|
||||
self.assertEqual(user_partititons[0].name, u'New Test name')
|
||||
self.assertEqual(len(user_partititons[0].groups), 2)
|
||||
self.assertEqual(user_partititons[0].groups[0].name, u'New Group Name')
|
||||
self.assertEqual(user_partititons[0].groups[1].name, u'Group C')
|
||||
|
||||
@@ -6,19 +6,17 @@ import ddt
|
||||
|
||||
from mock import patch
|
||||
from pytz import UTC
|
||||
from unittest import skipUnless
|
||||
from webob import Response
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.core.urlresolvers import reverse
|
||||
from contentstore.utils import reverse_usage_url, reverse_course_url
|
||||
from contentstore.utils import reverse_usage_url
|
||||
from contentstore.views.preview import StudioUserService
|
||||
|
||||
from contentstore.views.component import (
|
||||
component_handler, get_component_templates,
|
||||
SPLIT_TEST_COMPONENT_TYPE
|
||||
component_handler, get_component_templates
|
||||
)
|
||||
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
@@ -776,7 +774,6 @@ class TestEditItem(ItemTest):
|
||||
self.verify_publish_state(html_usage_key, PublishState.draft)
|
||||
|
||||
|
||||
@skipUnless(settings.FEATURES.get('ENABLE_GROUP_CONFIGURATIONS'), 'Tests Group Configurations feature')
|
||||
class TestEditSplitModule(ItemTest):
|
||||
"""
|
||||
Tests around editing instances of the split_test module.
|
||||
@@ -977,6 +974,12 @@ class TestEditSplitModule(ItemTest):
|
||||
group_id_to_child = split_test.group_id_to_child
|
||||
self.assertEqual(2, len(group_id_to_child))
|
||||
|
||||
# Test environment and Studio use different module systems
|
||||
# (CachingDescriptorSystem is used in tests, PreviewModuleSystem in Studio).
|
||||
# CachingDescriptorSystem doesn't have user service, that's needed for
|
||||
# SplitTestModule. So, in this line of code we add this service manually.
|
||||
split_test.runtime._services['user'] = StudioUserService(self.request) # pylint: disable=protected-access
|
||||
|
||||
# Call add_missing_groups method to add the missing group.
|
||||
split_test.add_missing_groups(self.request)
|
||||
split_test = self._assert_children(3)
|
||||
@@ -989,34 +992,6 @@ class TestEditSplitModule(ItemTest):
|
||||
split_test = self._assert_children(3)
|
||||
self.assertEqual(group_id_to_child, split_test.group_id_to_child)
|
||||
|
||||
def test_view_index_ok(self):
|
||||
"""
|
||||
Basic check that the groups configuration page responds correctly.
|
||||
"""
|
||||
if SPLIT_TEST_COMPONENT_TYPE not in self.course.advanced_modules:
|
||||
self.course.advanced_modules.append(SPLIT_TEST_COMPONENT_TYPE)
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
|
||||
url = reverse_course_url('group_configurations_list_handler', self.course.id)
|
||||
resp = self.client.get(url)
|
||||
self.assertContains(resp, self.course.display_name)
|
||||
self.assertContains(resp, 'First Partition')
|
||||
self.assertContains(resp, 'alpha')
|
||||
self.assertContains(resp, 'Second Partition')
|
||||
self.assertContains(resp, 'Group 1')
|
||||
|
||||
def test_view_index_disabled(self):
|
||||
"""
|
||||
Check that group configuration page is not displayed when turned off.
|
||||
"""
|
||||
if SPLIT_TEST_COMPONENT_TYPE in self.course.advanced_modules:
|
||||
self.course.advanced_modules.remove(SPLIT_TEST_COMPONENT_TYPE)
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
|
||||
url = reverse_course_url('group_configurations_list_handler', self.course.id)
|
||||
resp = self.client.get(url)
|
||||
self.assertContains(resp, "module is disabled")
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestComponentHandler(TestCase):
|
||||
|
||||
@@ -214,7 +214,6 @@ define([
|
||||
|
||||
"js/spec/models/component_template_spec",
|
||||
"js/spec/models/explicit_url_spec",
|
||||
"js/spec/models/group_configuration_spec",
|
||||
|
||||
"js/spec/utils/drag_and_drop_spec",
|
||||
"js/spec/utils/handle_iframe_binding_spec",
|
||||
@@ -223,6 +222,7 @@ define([
|
||||
"js/spec/views/baseview_spec",
|
||||
"js/spec/views/paging_spec",
|
||||
"js/spec/views/assets_spec",
|
||||
"js/spec/views/group_configuration_spec",
|
||||
|
||||
"js/spec/views/container_spec",
|
||||
"js/spec/views/unit_spec",
|
||||
@@ -235,8 +235,6 @@ define([
|
||||
"js/spec/views/modals/base_modal_spec",
|
||||
"js/spec/views/modals/edit_xblock_spec",
|
||||
|
||||
"js/spec/views/group_configuration_spec",
|
||||
|
||||
"js/spec/xblock/cms.runtime.v1_spec",
|
||||
|
||||
# these tests are run separately in the cms-squire suite, due to process
|
||||
|
||||
@@ -176,5 +176,6 @@ jasmine.getFixtures().fixturesPath += 'coffee/fixtures'
|
||||
define([
|
||||
"coffee/spec/views/assets_spec",
|
||||
"js/spec/video/translations_editor_spec",
|
||||
"js/spec/video/file_uploader_editor_spec"
|
||||
"js/spec/video/file_uploader_editor_spec",
|
||||
"js/spec/models/group_configuration_spec"
|
||||
])
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
define([
|
||||
'backbone', 'js/models/group'
|
||||
'underscore', 'underscore.string', 'backbone', 'gettext', 'js/models/group'
|
||||
],
|
||||
function (Backbone, GroupModel) {
|
||||
function (_, str, Backbone, gettext, GroupModel) {
|
||||
'use strict';
|
||||
var GroupCollection = Backbone.Collection.extend({
|
||||
model: GroupModel,
|
||||
comparator: 'order',
|
||||
/*
|
||||
* Return next index for the model.
|
||||
* @return {Number}
|
||||
*/
|
||||
nextOrder: function() {
|
||||
if(!this.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.last().get('order') + 1;
|
||||
},
|
||||
/**
|
||||
* Indicates if the collection is empty when all the models are empty
|
||||
* or the collection does not include any models.
|
||||
@@ -13,7 +25,86 @@ function (Backbone, GroupModel) {
|
||||
return this.length === 0 || this.every(function(m) {
|
||||
return m.isEmpty();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Return default name for the group.
|
||||
* @return {String}
|
||||
* @examples
|
||||
* Group A, Group B, Group AA, Group ZZZ etc.
|
||||
*/
|
||||
getNextDefaultGroupName: function () {
|
||||
var index = this.nextOrder(),
|
||||
usedNames = _.pluck(this.toJSON(), 'name'),
|
||||
name = '';
|
||||
|
||||
do {
|
||||
name = str.sprintf(gettext('Group %s'), this.getGroupId(index));
|
||||
index ++;
|
||||
} while (_.contains(usedNames, name));
|
||||
|
||||
return name;
|
||||
},
|
||||
|
||||
/*
|
||||
* Return group id for the default name of the group.
|
||||
* @param {Number} number Current index of the model in the collection.
|
||||
* @return {String}
|
||||
* @examples
|
||||
* A, B, AA in Group A, Group B, ..., Group AA, etc.
|
||||
*/
|
||||
getGroupId: (function () {
|
||||
/*
|
||||
Translators: Dictionary used for creation ids that are used in
|
||||
default group names. For example: A, B, AA in Group A,
|
||||
Group B, ..., Group AA, etc.
|
||||
*/
|
||||
var dict = gettext('ABCDEFGHIJKLMNOPQRSTUVWXYZ').split(''),
|
||||
len = dict.length,
|
||||
divide;
|
||||
|
||||
divide = function(numerator, denominator) {
|
||||
if (!_.isNumber(numerator) || !denominator) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
quotient: numerator / denominator,
|
||||
remainder: numerator % denominator
|
||||
};
|
||||
};
|
||||
|
||||
return function getId(number) {
|
||||
var accumulatedValues = '',
|
||||
result = divide(number, len),
|
||||
index;
|
||||
|
||||
if (result) {
|
||||
// subtract 1 to start the count with 0.
|
||||
index = Math.floor(result.quotient) - 1;
|
||||
|
||||
// Proceed by dividing the non-remainder part of the
|
||||
// dividend by the desired base until the result is less
|
||||
// than one.
|
||||
if (index < len) {
|
||||
// if index < 0, we do not need an additional power.
|
||||
if (index > -1) {
|
||||
// Get value for the next power.
|
||||
accumulatedValues += dict[index];
|
||||
}
|
||||
} else {
|
||||
// If we need more than 1 additional power.
|
||||
// Get value for the next powers.
|
||||
accumulatedValues += getId(index);
|
||||
}
|
||||
|
||||
// Accumulated values + the current reminder
|
||||
return accumulatedValues + dict[result.remainder];
|
||||
}
|
||||
|
||||
return String(number);
|
||||
};
|
||||
}())
|
||||
});
|
||||
|
||||
return GroupCollection;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
define([
|
||||
'backbone', 'gettext', 'backbone.associations'
|
||||
], function(Backbone, gettext) {
|
||||
'backbone', 'underscore', 'underscore.string', 'gettext',
|
||||
'backbone.associations'
|
||||
], function(Backbone, _, str, gettext) {
|
||||
'use strict';
|
||||
var Group = Backbone.AssociatedModel.extend({
|
||||
defaults: function() {
|
||||
return {
|
||||
name: '',
|
||||
version: null
|
||||
};
|
||||
version: null,
|
||||
order: null
|
||||
};
|
||||
},
|
||||
|
||||
isEmpty: function() {
|
||||
@@ -16,13 +18,14 @@ define([
|
||||
|
||||
toJSON: function() {
|
||||
return {
|
||||
id: this.get('id'),
|
||||
name: this.get('name'),
|
||||
version: this.get('version')
|
||||
};
|
||||
},
|
||||
|
||||
validate: function(attrs) {
|
||||
if (!attrs.name) {
|
||||
if (!str.trim(attrs.name)) {
|
||||
return {
|
||||
message: gettext('Group name is required'),
|
||||
attributes: { name: true }
|
||||
|
||||
@@ -11,7 +11,16 @@ function(Backbone, _, str, gettext, GroupModel, GroupCollection) {
|
||||
name: '',
|
||||
description: '',
|
||||
version: null,
|
||||
groups: new GroupCollection([]),
|
||||
groups: new GroupCollection([
|
||||
{
|
||||
name: gettext('Group A'),
|
||||
order: 0
|
||||
},
|
||||
{
|
||||
name: gettext('Group B'),
|
||||
order: 1
|
||||
}
|
||||
]),
|
||||
showGroups: false,
|
||||
editing: false
|
||||
};
|
||||
@@ -30,16 +39,16 @@ function(Backbone, _, str, gettext, GroupModel, GroupCollection) {
|
||||
},
|
||||
|
||||
setOriginalAttributes: function() {
|
||||
this._originalAttributes = this.toJSON();
|
||||
this._originalAttributes = this.parse(this.toJSON());
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this.set(this._originalAttributes);
|
||||
this.set(this._originalAttributes, { parse: true });
|
||||
},
|
||||
|
||||
isDirty: function() {
|
||||
return !_.isEqual(
|
||||
this._originalAttributes, this.toJSON()
|
||||
this._originalAttributes, this.parse(this.toJSON())
|
||||
);
|
||||
},
|
||||
|
||||
@@ -47,6 +56,16 @@ function(Backbone, _, str, gettext, GroupModel, GroupCollection) {
|
||||
return !this.get('name') && this.get('groups').isEmpty();
|
||||
},
|
||||
|
||||
parse: function(response) {
|
||||
var attrs = $.extend(true, {}, response);
|
||||
|
||||
_.each(attrs.groups, function(group, index) {
|
||||
group.order = group.order || index;
|
||||
});
|
||||
|
||||
return attrs;
|
||||
},
|
||||
|
||||
toJSON: function() {
|
||||
return {
|
||||
id: this.get('id'),
|
||||
@@ -64,7 +83,29 @@ function(Backbone, _, str, gettext, GroupModel, GroupCollection) {
|
||||
attributes: {name: true}
|
||||
};
|
||||
}
|
||||
|
||||
if (attrs.groups.length < 2) {
|
||||
return {
|
||||
message: gettext('There must be at least two groups'),
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
define([
|
||||
'backbone', 'coffee/src/main', 'js/models/group_configuration',
|
||||
'js/models/group', 'js/collections/group'
|
||||
'js/models/group', 'js/collections/group', 'squire'
|
||||
], function(
|
||||
Backbone, main, GroupConfigurationModel, GroupModel, GroupCollection
|
||||
Backbone, main, GroupConfigurationModel, GroupModel, GroupCollection, Squire
|
||||
) {
|
||||
'use strict';
|
||||
beforeEach(function() {
|
||||
@@ -32,11 +32,12 @@ define([
|
||||
expect(this.model.get('showGroups')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should be empty by default', function() {
|
||||
it('should have a collection with 2 groups by default', function() {
|
||||
var groups = this.model.get('groups');
|
||||
|
||||
expect(groups).toBeInstanceOf(GroupCollection);
|
||||
expect(this.model.isEmpty()).toBeTruthy();
|
||||
expect(groups.at(0).get('name')).toBe('Group A');
|
||||
expect(groups.at(1).get('name')).toBe('Group B');
|
||||
});
|
||||
|
||||
it('should be able to reset itself', function() {
|
||||
@@ -72,8 +73,6 @@ define([
|
||||
return deepAttributes(obj.attributes);
|
||||
} else if (obj instanceof Backbone.Collection) {
|
||||
return obj.map(deepAttributes);
|
||||
} else if (_.isArray(obj)) {
|
||||
return _.map(obj, deepAttributes);
|
||||
} else if (_.isObject(obj)) {
|
||||
var attributes = {};
|
||||
|
||||
@@ -114,14 +113,18 @@ define([
|
||||
'groups': [
|
||||
{
|
||||
'version': 1,
|
||||
'order': 0,
|
||||
'name': 'Group 1'
|
||||
}, {
|
||||
'version': 1,
|
||||
'order': 1,
|
||||
'name': 'Group 2'
|
||||
}
|
||||
]
|
||||
},
|
||||
model = new GroupConfigurationModel(serverModelSpec);
|
||||
model = new GroupConfigurationModel(
|
||||
serverModelSpec, { parse: true }
|
||||
);
|
||||
|
||||
expect(deepAttributes(model)).toEqual(clientModelSpec);
|
||||
expect(model.toJSON()).toEqual(serverModelSpec);
|
||||
@@ -145,11 +148,12 @@ define([
|
||||
|
||||
describe('GroupModel', function() {
|
||||
beforeEach(function() {
|
||||
this.model = new GroupModel();
|
||||
this.collection = new GroupCollection([{}]);
|
||||
this.model = this.collection.at(0);
|
||||
});
|
||||
|
||||
describe('Basic', function() {
|
||||
it('should have a name by default', function() {
|
||||
it('should have an empty name by default', function() {
|
||||
expect(this.model.get('name')).toEqual('');
|
||||
});
|
||||
|
||||
@@ -166,10 +170,41 @@ define([
|
||||
});
|
||||
|
||||
it('can pass validation', function() {
|
||||
var model = new GroupModel({ name: 'a' });
|
||||
var model = new GroupConfigurationModel({ name: 'foo' });
|
||||
|
||||
expect(model.isValid()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('requires at least two groups', function() {
|
||||
var group1 = new GroupModel({ name: 'Group A' }),
|
||||
group2 = new GroupModel({ name: 'Group B' }),
|
||||
model = new GroupConfigurationModel({ name: 'foo' });
|
||||
|
||||
model.get('groups').reset([group1]);
|
||||
expect(model.isValid()).toBeFalsy();
|
||||
|
||||
model.get('groups').add(group2);
|
||||
expect(model.isValid()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('requires a valid group', function() {
|
||||
var group = new GroupModel(),
|
||||
model = new GroupConfigurationModel({ name: 'foo' });
|
||||
|
||||
model.get('groups').reset([group]);
|
||||
|
||||
expect(model.isValid()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('requires all groups to be valid', function() {
|
||||
var group1 = new GroupModel({ name: 'Group A' }),
|
||||
group2 = new GroupModel(),
|
||||
model = new GroupConfigurationModel({ name: 'foo' });
|
||||
|
||||
model.get('groups').reset([group1, group2]);
|
||||
|
||||
expect(model.isValid()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -183,15 +218,94 @@ define([
|
||||
});
|
||||
|
||||
it('is empty if all groups are empty', function() {
|
||||
this.collection.add([{}, {}, {}]);
|
||||
this.collection.add([{ name: '' }, { name: '' }, { name: '' }]);
|
||||
|
||||
expect(this.collection.isEmpty()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('is not empty if a group is not empty', function() {
|
||||
this.collection.add([{}, { name: 'full' }, {} ]);
|
||||
this.collection.add([
|
||||
{ name: '' }, { name: 'full' }, { name: '' }
|
||||
]);
|
||||
|
||||
expect(this.collection.isEmpty()).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('getGroupId', function () {
|
||||
var collection, injector, mockGettext, initializeGroupModel;
|
||||
|
||||
mockGettext = function (returnedValue) {
|
||||
var injector = new Squire();
|
||||
|
||||
injector.mock('gettext', function () {
|
||||
return function () { return returnedValue; };
|
||||
});
|
||||
|
||||
return injector;
|
||||
};
|
||||
|
||||
initializeGroupModel = function (dict, that) {
|
||||
runs(function() {
|
||||
injector = mockGettext(dict);
|
||||
injector.require(['js/collections/group'],
|
||||
function(GroupCollection) {
|
||||
collection = new GroupCollection();
|
||||
});
|
||||
});
|
||||
|
||||
waitsFor(function() {
|
||||
return collection;
|
||||
}, 'GroupModel was not instantiated', 500);
|
||||
|
||||
that.after(function () {
|
||||
collection = null;
|
||||
injector.clean();
|
||||
injector.remove();
|
||||
});
|
||||
};
|
||||
|
||||
it('returns correct ids', function () {
|
||||
var collection = new GroupCollection();
|
||||
|
||||
expect(collection.getGroupId(0)).toBe('A');
|
||||
expect(collection.getGroupId(1)).toBe('B');
|
||||
expect(collection.getGroupId(25)).toBe('Z');
|
||||
expect(collection.getGroupId(702)).toBe('AAA');
|
||||
expect(collection.getGroupId(704)).toBe('AAC');
|
||||
expect(collection.getGroupId(475253)).toBe('ZZZZ');
|
||||
expect(collection.getGroupId(475254)).toBe('AAAAA');
|
||||
expect(collection.getGroupId(475279)).toBe('AAAAZ');
|
||||
});
|
||||
|
||||
it('just 1 character in the dictionary', function () {
|
||||
initializeGroupModel('1', this);
|
||||
runs(function() {
|
||||
expect(collection.getGroupId(0)).toBe('1');
|
||||
expect(collection.getGroupId(1)).toBe('11');
|
||||
expect(collection.getGroupId(5)).toBe('111111');
|
||||
});
|
||||
});
|
||||
|
||||
it('allow to use unicode characters in the dict', function () {
|
||||
initializeGroupModel('ö诶úeœ', this);
|
||||
runs(function() {
|
||||
expect(collection.getGroupId(0)).toBe('ö');
|
||||
expect(collection.getGroupId(1)).toBe('诶');
|
||||
expect(collection.getGroupId(5)).toBe('öö');
|
||||
expect(collection.getGroupId(29)).toBe('œœ');
|
||||
expect(collection.getGroupId(30)).toBe('ööö');
|
||||
expect(collection.getGroupId(43)).toBe('öúe');
|
||||
});
|
||||
});
|
||||
|
||||
it('return initial value if dictionary is empty', function () {
|
||||
initializeGroupModel('', this);
|
||||
runs(function() {
|
||||
expect(collection.getGroupId(0)).toBe('0');
|
||||
expect(collection.getGroupId(5)).toBe('5');
|
||||
expect(collection.getGroupId(30)).toBe('30');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,13 +3,15 @@ define([
|
||||
'js/collections/group_configuration',
|
||||
'js/views/group_configuration_details',
|
||||
'js/views/group_configurations_list', 'js/views/group_configuration_edit',
|
||||
'js/views/group_configuration_item', 'js/views/feedback_notification',
|
||||
'js/spec_helpers/create_sinon', 'js/spec_helpers/edit_helpers',
|
||||
'jasmine-stealth'
|
||||
'js/views/group_configuration_item', 'js/models/group',
|
||||
'js/collections/group', 'js/views/group_edit',
|
||||
'js/views/feedback_notification', 'js/spec_helpers/create_sinon',
|
||||
'js/spec_helpers/edit_helpers', 'jasmine-stealth'
|
||||
], function(
|
||||
Course, GroupConfigurationModel, GroupConfigurationCollection,
|
||||
GroupConfigurationDetails, GroupConfigurationsList, GroupConfigurationEdit,
|
||||
GroupConfigurationItem, Notification, create_sinon, view_helpers
|
||||
GroupConfigurationItem, GroupModel, GroupCollection, GroupEdit,
|
||||
Notification, create_sinon, view_helpers
|
||||
) {
|
||||
'use strict';
|
||||
var SELECTORS = {
|
||||
@@ -17,13 +19,15 @@ define([
|
||||
editView: '.group-configuration-edit',
|
||||
itemView: '.group-configurations-list-item',
|
||||
group: '.group',
|
||||
groupFields: '.groups-fields',
|
||||
name: '.group-configuration-name',
|
||||
description: '.group-configuration-description',
|
||||
groupsCount: '.group-configuration-groups-count',
|
||||
groupsAllocation: '.group-allocation',
|
||||
errorMessage: '.group-configuration-edit-error',
|
||||
inputGroupName: '.group-name',
|
||||
inputName: '.group-configuration-name-input',
|
||||
inputDescription: '.group-configuration-description-input'
|
||||
inputDescription: '.group-configuration-description-input',
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
@@ -59,6 +63,13 @@ define([
|
||||
return _.every(values, function (value, key) {
|
||||
return this.actual.get(key) === value;
|
||||
}.bind(this));
|
||||
},
|
||||
toHaveDefaultNames: function (values) {
|
||||
var actualValues = $.map(this.actual, function (item) {
|
||||
return $(item).val();
|
||||
});
|
||||
|
||||
return _.isEqual(actualValues, values);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -90,7 +101,7 @@ define([
|
||||
});
|
||||
|
||||
it('should show groups appropriately', function() {
|
||||
this.model.get('groups').add([{}, {}, {}]);
|
||||
this.model.get('groups').add([{}]);
|
||||
this.model.set('showGroups', false);
|
||||
this.view.$('.show-groups').click();
|
||||
|
||||
@@ -104,7 +115,7 @@ define([
|
||||
});
|
||||
|
||||
it('should hide groups appropriately', function() {
|
||||
this.model.get('groups').add([{}, {}, {}]);
|
||||
this.model.get('groups').add([{}]);
|
||||
this.model.set('showGroups', true);
|
||||
this.view.$('.hide-groups').click();
|
||||
|
||||
@@ -129,7 +140,9 @@ define([
|
||||
|
||||
beforeEach(function() {
|
||||
view_helpers.installViewTemplates();
|
||||
view_helpers.installTemplate('group-configuration-edit');
|
||||
view_helpers.installTemplates([
|
||||
'group-configuration-edit', 'group-edit'
|
||||
]);
|
||||
|
||||
this.model = new GroupConfigurationModel({
|
||||
name: 'Configuration',
|
||||
@@ -152,10 +165,18 @@ define([
|
||||
});
|
||||
});
|
||||
|
||||
it ('should allow you to create new groups', function() {
|
||||
var numGroups = this.model.get('groups').length;
|
||||
this.view.$('.action-add-group').click();
|
||||
expect(this.model.get('groups').length).toEqual(numGroups + 1);
|
||||
});
|
||||
|
||||
it('should save properly', function() {
|
||||
var requests = create_sinon.requests(this),
|
||||
notificationSpy = view_helpers.createNotificationSpy();
|
||||
notificationSpy = view_helpers.createNotificationSpy(),
|
||||
groups;
|
||||
|
||||
this.view.$('.action-add-group').click();
|
||||
setValuesToInputs(this.view, {
|
||||
inputName: 'New Configuration',
|
||||
inputDescription: 'New Description'
|
||||
@@ -170,6 +191,10 @@ define([
|
||||
name: 'New Configuration',
|
||||
description: 'New Description'
|
||||
});
|
||||
|
||||
groups = this.model.get('groups');
|
||||
expect(groups.length).toBe(3);
|
||||
expect(groups.at(2).get('name')).toBe('Group C');
|
||||
expect(this.view.$el).not.toExist();
|
||||
});
|
||||
|
||||
@@ -185,10 +210,14 @@ define([
|
||||
});
|
||||
|
||||
it('does not save on cancel', function() {
|
||||
this.view.$('.action-add-group').click();
|
||||
setValuesToInputs(this.view, {
|
||||
inputName: 'New Configuration',
|
||||
inputDescription: 'New Description'
|
||||
});
|
||||
|
||||
expect(this.model.get('groups').length).toBe(3);
|
||||
|
||||
this.view.$('.action-cancel').click();
|
||||
expect(this.model).toBeCorrectValuesInModel({
|
||||
name: 'Configuration',
|
||||
@@ -197,6 +226,7 @@ define([
|
||||
// Model is still exist in the collection
|
||||
expect(this.collection.indexOf(this.model)).toBeGreaterThan(-1);
|
||||
expect(this.collection.length).toBe(1);
|
||||
expect(this.model.get('groups').length).toBe(2);
|
||||
});
|
||||
|
||||
it('should be removed on cancel if it is a new item', function() {
|
||||
@@ -236,6 +266,69 @@ define([
|
||||
expect(this.view.$(SELECTORS.errorMessage)).not.toExist();
|
||||
expect(requests.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should have appropriate class names on focus/blur', function () {
|
||||
var groupInput = this.view.$(SELECTORS.inputGroupName).first(),
|
||||
groupFields = this.view.$(SELECTORS.groupFields);
|
||||
|
||||
groupInput.focus();
|
||||
expect(groupFields).toHaveClass('is-focused');
|
||||
groupInput.blur();
|
||||
expect(groupFields).not.toHaveClass('is-focused');
|
||||
});
|
||||
|
||||
describe('removes all newly created groups on cancel', function () {
|
||||
it('if the model has a non-empty groups', function() {
|
||||
var groups = this.model.get('groups');
|
||||
|
||||
this.view.render();
|
||||
groups.add([{ name: 'non-empty' }]);
|
||||
expect(groups.length).toEqual(3);
|
||||
this.view.$('.action-cancel').click();
|
||||
// Restore to default state (2 groups by default).
|
||||
expect(groups.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('if the model has no non-empty groups', function() {
|
||||
var groups = this.model.get('groups');
|
||||
|
||||
this.view.render();
|
||||
groups.add([{}, {}, {}]);
|
||||
expect(groups.length).toEqual(5);
|
||||
this.view.$('.action-cancel').click();
|
||||
// Restore to default state (2 groups by default).
|
||||
expect(groups.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('groups have correct default names and placeholders', function () {
|
||||
var group1 = new GroupModel({ name: 'Group A' }),
|
||||
group2 = new GroupModel({ name: 'Group B' }),
|
||||
collection = this.model.get('groups');
|
||||
|
||||
collection.reset([group1, group2]); // Group A, Group B
|
||||
this.view.$('.action-add-group').click(); // Add Group C
|
||||
this.view.$('.action-add-group').click(); // Add Group D
|
||||
this.view.$('.action-add-group').click(); // Add Group E
|
||||
|
||||
expect(this.view.$(SELECTORS.inputGroupName)).toHaveDefaultNames([
|
||||
'Group A', 'Group B', 'Group C', 'Group D', 'Group E'
|
||||
]);
|
||||
|
||||
// Remove Group B
|
||||
this.view.$('.group-1 .action-close').click();
|
||||
|
||||
expect(this.view.$(SELECTORS.inputGroupName)).toHaveDefaultNames([
|
||||
'Group A', 'Group C', 'Group D', 'Group E'
|
||||
]);
|
||||
|
||||
this.view.$('.action-add-group').click(); // Add Group F
|
||||
this.view.$('.action-add-group').click(); // Add Group G
|
||||
|
||||
expect(this.view.$(SELECTORS.inputGroupName)).toHaveDefaultNames([
|
||||
'Group A', 'Group C', 'Group D', 'Group E', 'Group F', 'Group G'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GroupConfigurationsList', function() {
|
||||
@@ -254,7 +347,7 @@ define([
|
||||
expect(this.view.$el).toContainText(
|
||||
'You haven\'t created any group configurations yet.'
|
||||
);
|
||||
expect(this.view.$el).toContain('.new-button');
|
||||
expect(this.view.$('.new-button')).toExist();
|
||||
expect(this.view.$(SELECTORS.itemView)).not.toExist();
|
||||
});
|
||||
|
||||
@@ -273,8 +366,9 @@ define([
|
||||
|
||||
describe('GroupConfigurationItem', function() {
|
||||
beforeEach(function() {
|
||||
view_helpers.installTemplate('group-configuration-edit', true);
|
||||
view_helpers.installTemplate('group-configuration-details');
|
||||
view_helpers.installTemplates([
|
||||
'group-configuration-edit', 'group-configuration-details'
|
||||
], true);
|
||||
this.model = new GroupConfigurationModel({ id: 0 });
|
||||
this.collection = new GroupConfigurationCollection([ this.model ]);
|
||||
this.view = new GroupConfigurationItem({
|
||||
@@ -294,6 +388,35 @@ define([
|
||||
expect(this.view.$(SELECTORS.editView)).not.toExist();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GroupEdit', function() {
|
||||
beforeEach(function() {
|
||||
view_helpers.installTemplate('group-edit', true);
|
||||
|
||||
this.model = new GroupModel({
|
||||
name: 'Group A'
|
||||
});
|
||||
|
||||
this.collection = new GroupCollection([this.model]);
|
||||
|
||||
this.view = new GroupEdit({
|
||||
model: this.model
|
||||
});
|
||||
});
|
||||
|
||||
describe('Basic', function () {
|
||||
it('can render properly', function() {
|
||||
this.view.render();
|
||||
expect(this.view.$('.group-name').val()).toBe('Group A');
|
||||
expect(this.view.$('.group-allocation')).toContainText('100%');
|
||||
});
|
||||
|
||||
it ('can delete itself', function() {
|
||||
this.view.render().$('.action-close').click();
|
||||
expect(this.collection.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
*/
|
||||
define(["jquery", "js/views/feedback_notification", "js/spec_helpers/create_sinon"],
|
||||
function($, NotificationView, create_sinon) {
|
||||
var installTemplate, installViewTemplates, createNotificationSpy, verifyNotificationShowing,
|
||||
verifyNotificationHidden;
|
||||
var installTemplate, installTemplates, installViewTemplates, createNotificationSpy,
|
||||
verifyNotificationShowing, verifyNotificationHidden;
|
||||
|
||||
installTemplate = function(templateName, isFirst) {
|
||||
var template = readFixtures(templateName + '.underscore'),
|
||||
templateId = templateName + '-tpl';
|
||||
|
||||
if (isFirst) {
|
||||
setFixtures($("<script>", { id: templateId, type: "text/template" }).text(template));
|
||||
} else {
|
||||
@@ -16,6 +17,19 @@ define(["jquery", "js/views/feedback_notification", "js/spec_helpers/create_sino
|
||||
}
|
||||
};
|
||||
|
||||
installTemplates = function(templateNames, isFirst) {
|
||||
if (!$.isArray(templateNames)) {
|
||||
templateNames = [templateNames];
|
||||
}
|
||||
|
||||
$.each(templateNames, function(index, templateName) {
|
||||
installTemplate(templateName, isFirst);
|
||||
if (isFirst) {
|
||||
isFirst = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
installViewTemplates = function(append) {
|
||||
installTemplate('system-feedback', !append);
|
||||
appendSetFixtures('<div id="page-notification"></div>');
|
||||
@@ -41,6 +55,7 @@ define(["jquery", "js/views/feedback_notification", "js/spec_helpers/create_sino
|
||||
|
||||
return {
|
||||
'installTemplate': installTemplate,
|
||||
'installTemplates': installTemplates,
|
||||
'installViewTemplates': installViewTemplates,
|
||||
'createNotificationSpy': createNotificationSpy,
|
||||
'verifyNotificationShowing': verifyNotificationShowing,
|
||||
|
||||
@@ -42,13 +42,13 @@ function(BaseView, _, gettext) {
|
||||
this.model.set('editing', true);
|
||||
},
|
||||
|
||||
showGroups: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
showGroups: function(event) {
|
||||
if(event && event.preventDefault) { event.preventDefault(); }
|
||||
this.model.set('showGroups', true);
|
||||
},
|
||||
|
||||
hideGroups: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
hideGroups: function(event) {
|
||||
if(event && event.preventDefault) { event.preventDefault(); }
|
||||
this.model.set('showGroups', false);
|
||||
},
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
define([
|
||||
'js/views/baseview', 'underscore', 'jquery', 'gettext'
|
||||
'js/views/baseview', 'underscore', 'jquery', 'gettext',
|
||||
'js/views/group_edit'
|
||||
],
|
||||
function(BaseView, _, $, gettext) {
|
||||
function(BaseView, _, $, gettext, GroupEdit) {
|
||||
'use strict';
|
||||
var GroupConfigurationEdit = BaseView.extend({
|
||||
tagName: 'div',
|
||||
events: {
|
||||
'change .group-configuration-name-input': 'setName',
|
||||
'change .group-configuration-description-input': 'setDescription',
|
||||
"click .action-add-group": "createGroup",
|
||||
'focus .input-text': 'onFocus',
|
||||
'blur .input-text': 'onBlur',
|
||||
'submit': 'setAndClose',
|
||||
@@ -24,8 +26,14 @@ function(BaseView, _, $, gettext) {
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
var groups;
|
||||
|
||||
this.template = this.loadTemplate('group-configuration-edit');
|
||||
this.listenTo(this.model, 'invalid', this.render);
|
||||
groups = this.model.get('groups');
|
||||
this.listenTo(groups, 'add', this.addOne);
|
||||
this.listenTo(groups, 'reset', this.addAll);
|
||||
this.listenTo(groups, 'all', this.render);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
@@ -37,10 +45,31 @@ function(BaseView, _, $, gettext) {
|
||||
isNew: this.model.isNew(),
|
||||
error: this.model.validationError
|
||||
}));
|
||||
this.addAll();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
addOne: function(group) {
|
||||
var view = new GroupEdit({ model: group });
|
||||
this.$('ol.groups').append(view.render().el);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
addAll: function() {
|
||||
this.model.get('groups').each(this.addOne, this);
|
||||
},
|
||||
|
||||
createGroup: function(event) {
|
||||
if(event && event.preventDefault) { event.preventDefault(); }
|
||||
var collection = this.model.get('groups');
|
||||
collection.add([{
|
||||
name: collection.getNextDefaultGroupName(),
|
||||
order: collection.nextOrder()
|
||||
}]);
|
||||
},
|
||||
|
||||
setName: function(event) {
|
||||
if(event && event.preventDefault) { event.preventDefault(); }
|
||||
this.model.set(
|
||||
@@ -62,6 +91,16 @@ function(BaseView, _, $, gettext) {
|
||||
this.setName();
|
||||
this.setDescription();
|
||||
|
||||
_.each(this.$('.groups li'), function(li, i) {
|
||||
var group = this.model.get('groups').at(i);
|
||||
|
||||
if(group) {
|
||||
group.set({
|
||||
'name': $('.group-name', li).val()
|
||||
});
|
||||
}
|
||||
}, this);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ define([
|
||||
|
||||
addOne: function(event) {
|
||||
if(event && event.preventDefault) { event.preventDefault(); }
|
||||
this.collection.add([{editing: true}]);
|
||||
this.collection.add([{ editing: true }]);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
69
cms/static/js/views/group_edit.js
Normal file
69
cms/static/js/views/group_edit.js
Normal file
@@ -0,0 +1,69 @@
|
||||
define([
|
||||
'js/views/baseview', 'underscore', 'underscore.string', 'jquery', 'gettext'
|
||||
],
|
||||
function(BaseView, _, str, $, gettext) {
|
||||
'use strict';
|
||||
_.str = str; // used in template
|
||||
var GroupEdit = BaseView.extend({
|
||||
tagName: 'li',
|
||||
events: {
|
||||
'click .action-close': 'removeGroup',
|
||||
'change .group-name': 'changeName',
|
||||
'focus .groups-fields input': 'onFocus',
|
||||
'blur .groups-fields input': 'onBlur'
|
||||
},
|
||||
|
||||
className: function() {
|
||||
var index = this.model.collection.indexOf(this.model);
|
||||
return 'field-group group group-' + index;
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.template = this.loadTemplate('group-edit');
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var collection = this.model.collection,
|
||||
index = collection.indexOf(this.model);
|
||||
|
||||
this.$el.html(this.template({
|
||||
name: this.model.escape('name'),
|
||||
allocation: this.getAllocation(),
|
||||
index: index,
|
||||
error: this.model.validationError
|
||||
}));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
changeName: function(event) {
|
||||
if(event && event.preventDefault) { event.preventDefault(); }
|
||||
this.model.set({
|
||||
name: this.$('.group-name').val()
|
||||
}, { silent: true });
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
removeGroup: function(event) {
|
||||
if(event && event.preventDefault) { event.preventDefault(); }
|
||||
this.model.collection.remove(this.model);
|
||||
return this.remove();
|
||||
},
|
||||
|
||||
getAllocation: function() {
|
||||
return Math.floor(100 / this.model.collection.length);
|
||||
},
|
||||
|
||||
onFocus: function () {
|
||||
this.$el.closest('.groups-fields').addClass('is-focused');
|
||||
},
|
||||
|
||||
onBlur: function () {
|
||||
this.$el.closest('.groups-fields').removeClass('is-focused');
|
||||
}
|
||||
});
|
||||
|
||||
return GroupEdit;
|
||||
});
|
||||
@@ -50,6 +50,7 @@ lib_paths:
|
||||
- xmodule_js/common_static/js/vendor/jasmine-imagediff.js
|
||||
- xmodule_js/common_static/js/vendor/jasmine.async.js
|
||||
- xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js
|
||||
- xmodule_js/common_static/js/vendor/domReady.js
|
||||
- xmodule_js/common_static/js/vendor/URI.min.js
|
||||
- xmodule_js/src/xmodule.js
|
||||
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
|
||||
|
||||
@@ -250,6 +250,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.groups-fields,
|
||||
.group-configuration-fields {
|
||||
@extend %cont-no-list;
|
||||
|
||||
@@ -361,6 +362,10 @@
|
||||
margin-left: ($baseline*0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&.group-allocation {
|
||||
color: $gray-l1;
|
||||
}
|
||||
}
|
||||
|
||||
label.required {
|
||||
@@ -371,11 +376,67 @@
|
||||
content: "*";
|
||||
}
|
||||
}
|
||||
|
||||
.field-group {
|
||||
@include clearfix();
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
padding: ($baseline/4) 0 0 0;
|
||||
|
||||
.group-allocation,
|
||||
.field {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: 0 3% 0 0;
|
||||
}
|
||||
|
||||
.group-allocation {
|
||||
max-width: 10%;
|
||||
min-width: 5%;
|
||||
color: $gray-l1;
|
||||
}
|
||||
|
||||
.field {
|
||||
position: relative;
|
||||
|
||||
&.long {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
&.short {
|
||||
width: 10%;
|
||||
}
|
||||
}
|
||||
|
||||
.action-close {
|
||||
@include transition(color 0.25s ease-in-out);
|
||||
@include font-size(22);
|
||||
display: inline-block;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: $blue-l3;
|
||||
vertical-align: middle;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-configuration-fields {
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
|
||||
.action-add-group {
|
||||
@extend %ui-btn-flat-outline;
|
||||
@include font-size(16);
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: ($baseline*1.5) 0 0 0;
|
||||
padding: ($baseline/2);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<%block name="bodyclass">is-signedin course view-group-configurations</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
% for template_name in ["group-configuration-details", "group-configuration-edit", "no-group-configurations", "basic-modal", "modal-button"]:
|
||||
% for template_name in ["group-configuration-details", "group-configuration-edit", "no-group-configurations", "group-edit", "basic-modal", "modal-button"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
@@ -23,7 +23,7 @@
|
||||
require(["domReady!", "js/collections/group_configuration", "js/views/pages/group_configurations"],
|
||||
function(doc, GroupConfigurationCollection, GroupConfigurationsPage) {
|
||||
% if configurations is not None:
|
||||
var collection = new GroupConfigurationCollection(${json.dumps(configurations)});
|
||||
var collection = new GroupConfigurationCollection(${json.dumps(configurations)}, { parse: true });
|
||||
|
||||
collection.url = "${group_configuration_url}";
|
||||
new GroupConfigurationsPage({
|
||||
|
||||
@@ -25,6 +25,13 @@
|
||||
<span class="tip tip-stacked"><%= gettext("Optional long description") %></span>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="groups-fields">
|
||||
<legend class="sr"><%= gettext("Group information") %></legend>
|
||||
<label class="groups-fields-label required"><%= gettext("Groups") %></label>
|
||||
<span class="tip tip-stacked"><%= gettext("Name of the groups that students will be assigned to, for example, Control, Video, Problems. You must have two or more groups.") %></span>
|
||||
<ol class="groups list-input enum"></ol>
|
||||
<button class="action action-add-group"><i class="icon-plus"></i> <%= gettext("Add another group") %></button>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="action action-primary" type="submit"><% if (isNew) { print(gettext("Create")) } else { print(gettext("Save")) } %></button>
|
||||
|
||||
4
cms/templates/js/group-edit.underscore
Normal file
4
cms/templates/js/group-edit.underscore
Normal file
@@ -0,0 +1,4 @@
|
||||
<div class="input-wrap field long text required field-add-group-name group-<%= index %>-name
|
||||
<% if (error && error.attributes && error.attributes.name) { print('error'); } %>"><input name="group-<%= index %>-name" class="group-name long" value="<%= name %>" type="text">
|
||||
</div><div class="group-allocation"><%= allocation %>%</div>
|
||||
<a href="" class="action action-close"><i class="icon-remove-sign"></i> <span class="sr"><%= gettext("delete group") %></span></a>
|
||||
@@ -126,7 +126,7 @@ class ContainerPage(PageObject):
|
||||
"""
|
||||
Click the "add missing groups" link.
|
||||
"""
|
||||
click_css(self, '.add-missing-groups-button')
|
||||
self.q(css='.add-missing-groups-button').first.click()
|
||||
|
||||
def missing_groups_button_present(self):
|
||||
"""
|
||||
|
||||
@@ -55,6 +55,13 @@ class GroupConfiguration(object):
|
||||
css = 'a.group-toggle'
|
||||
self.find_css(css).first.click()
|
||||
|
||||
def add_group(self):
|
||||
"""
|
||||
Add new group.
|
||||
"""
|
||||
css = 'button.action-add-group'
|
||||
self.find_css(css).first.click()
|
||||
|
||||
def get_text(self, css):
|
||||
"""
|
||||
Return text for the defined by css locator.
|
||||
@@ -144,10 +151,10 @@ class GroupConfiguration(object):
|
||||
"""
|
||||
css = '.group'
|
||||
|
||||
def group_selector(config_index, group_index):
|
||||
return self.get_selector('.groups-{} .group-{} '.format(config_index, group_index))
|
||||
def group_selector(group_index):
|
||||
return self.get_selector('.group-{} '.format(group_index))
|
||||
|
||||
return [Group(self.page, group_selector(self.index, index)) for index, element in enumerate(self.find_css(css))]
|
||||
return [Group(self.page, group_selector(index)) for index, element in enumerate(self.find_css(css))]
|
||||
|
||||
def __repr__(self):
|
||||
return "<{}:{}>".format(self.__class__.__name__, self.name)
|
||||
@@ -170,11 +177,19 @@ class Group(object):
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
Return group name.
|
||||
Return the name of the group .
|
||||
"""
|
||||
css = '.group-name'
|
||||
return self.find_css(css).first.text[0]
|
||||
|
||||
@name.setter
|
||||
def name(self, value):
|
||||
"""
|
||||
Set the name for the group.
|
||||
"""
|
||||
css = '.group-name'
|
||||
self.find_css(css).first.fill(value)
|
||||
|
||||
@property
|
||||
def allocation(self):
|
||||
"""
|
||||
@@ -183,5 +198,12 @@ class Group(object):
|
||||
css = '.group-allocation'
|
||||
return self.find_css(css).first.text[0]
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Remove the group.
|
||||
"""
|
||||
css = '.action-close'
|
||||
return self.find_css(css).first.click()
|
||||
|
||||
def __repr__(self):
|
||||
return "<{}:{}>".format(self.__class__.__name__, self.name)
|
||||
|
||||
@@ -11,7 +11,7 @@ from ..pages.studio.component_editor import ComponentEditorView
|
||||
from ..pages.studio.utils import add_discussion
|
||||
|
||||
from unittest import skip
|
||||
from bok_choy.promise import Promise
|
||||
|
||||
|
||||
class ContainerBase(UniqueCourseTest):
|
||||
"""
|
||||
@@ -93,30 +93,6 @@ class ContainerBase(UniqueCourseTest):
|
||||
break
|
||||
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):
|
||||
"""
|
||||
Perform the supplied action and then verify the resulting ordering.
|
||||
|
||||
@@ -18,11 +18,51 @@ from ..pages.studio.auto_auth import AutoAuthPage
|
||||
from ..pages.studio.utils import add_advanced_component
|
||||
from ..pages.xblock.utils import wait_for_xblock_initialization
|
||||
from .helpers import UniqueCourseTest
|
||||
|
||||
from test_studio_container import ContainerBase
|
||||
|
||||
|
||||
class SplitTest(ContainerBase):
|
||||
class SplitTestMixin(object):
|
||||
"""
|
||||
Mixin that contains useful methods for split_test module testing.
|
||||
"""
|
||||
def verify_groups(self, container, active_groups, inactive_groups, verify_missing_groups_not_present=True):
|
||||
"""
|
||||
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:
|
||||
self.verify_add_missing_groups_button_not_present(container)
|
||||
|
||||
def verify_add_missing_groups_button_not_present(self, container):
|
||||
"""
|
||||
Checks that the "add missing gorups" button/link is not present.
|
||||
"""
|
||||
def missing_groups_button_not_present():
|
||||
button_present = container.missing_groups_button_present()
|
||||
return (not button_present, not button_present)
|
||||
|
||||
Promise(missing_groups_button_not_present, "Add missing groups button should not be showing.").fulfill()
|
||||
|
||||
|
||||
class SplitTest(ContainerBase, SplitTestMixin):
|
||||
"""
|
||||
Tests for creating and editing split test instances in Studio.
|
||||
"""
|
||||
@@ -58,21 +98,6 @@ class SplitTest(ContainerBase):
|
||||
|
||||
self.user = course_fix.user
|
||||
|
||||
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)
|
||||
if verify_missing_groups_not_present:
|
||||
self.verify_add_missing_groups_button_not_present(container)
|
||||
|
||||
def verify_add_missing_groups_button_not_present(self, container):
|
||||
"""
|
||||
Checks that the "add missing gorups" button/link is not present.
|
||||
"""
|
||||
def missing_groups_button_not_present():
|
||||
button_present = container.missing_groups_button_present()
|
||||
return (not button_present, not button_present)
|
||||
|
||||
Promise(missing_groups_button_not_present, "Add missing groups button should not be showing.").fulfill()
|
||||
|
||||
def create_poorly_configured_split_instance(self):
|
||||
"""
|
||||
Creates a split test instance with a missing group and an inactive group.
|
||||
@@ -210,7 +235,7 @@ class SettingsMenuTest(UniqueCourseTest):
|
||||
|
||||
|
||||
@skipUnless(os.environ.get('FEATURE_GROUP_CONFIGURATIONS'), 'Tests Group Configurations feature')
|
||||
class GroupConfigurationsTest(ContainerBase):
|
||||
class GroupConfigurationsTest(ContainerBase, SplitTestMixin):
|
||||
"""
|
||||
Tests that Group Configurations page works correctly with previously
|
||||
added configurations in Studio
|
||||
@@ -266,9 +291,10 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
|
||||
if groups:
|
||||
allocation = int(math.floor(100 / len(groups)))
|
||||
for index, group in enumerate(groups):
|
||||
self.assertEqual(group, config.groups[index].name)
|
||||
self.assertEqual(str(allocation) + "%", config.groups[index].allocation)
|
||||
self.assertEqual(groups, [group.name for group in config.groups])
|
||||
for group in config.groups:
|
||||
self.assertEqual(str(allocation) + "%", group.allocation)
|
||||
|
||||
# Collapse the configuration
|
||||
config.toggle()
|
||||
|
||||
@@ -336,11 +362,11 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
Scenario: Ensure that the group configuration can be created and edited correctly.
|
||||
Given I have a course without group configurations
|
||||
When I click button 'Create new Group Configuration'
|
||||
And I set new name and description
|
||||
And I set new name and description, change name for the 2nd default group, add one new group
|
||||
And I click button 'Create'
|
||||
Then I see the new group configuration is added
|
||||
Then I see the new group configuration is added and has correct data
|
||||
When I edit the group group_configuration
|
||||
And I change the name and description
|
||||
And I change the name and description, add new group, remove old one and change name for the Group A
|
||||
And I click button 'Save'
|
||||
Then I see the group configuration is saved successfully and has the new data
|
||||
"""
|
||||
@@ -351,15 +377,19 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
config = self.page.group_configurations()[0]
|
||||
config.name = "New Group Configuration Name"
|
||||
config.description = "New Description of the group configuration."
|
||||
self.assertEqual(config.get_text('.action-primary'), "CREATE")
|
||||
config.groups[1].name = "New Group Name"
|
||||
# Add new group
|
||||
config.add_group() # Group C
|
||||
|
||||
# Save the configuration
|
||||
self.assertEqual(config.get_text('.action-primary'), "CREATE")
|
||||
config.save()
|
||||
|
||||
self._assert_fields(
|
||||
config,
|
||||
name="New Group Configuration Name",
|
||||
description="New Description of the group configuration.",
|
||||
groups=["Group A", "Group B"]
|
||||
groups=["Group A", "New Group Name", "Group C"]
|
||||
)
|
||||
|
||||
# Edit the group configuration
|
||||
@@ -369,13 +399,20 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
config.name = "Second Group Configuration Name"
|
||||
config.description = "Second Description of the group configuration."
|
||||
self.assertEqual(config.get_text('.action-primary'), "SAVE")
|
||||
# Add new group
|
||||
config.add_group() # Group D
|
||||
# Remove group with name "New Group Name"
|
||||
config.groups[1].remove()
|
||||
# Rename Group A
|
||||
config.groups[0].name = "First Group"
|
||||
# Save the configuration
|
||||
config.save()
|
||||
|
||||
self._assert_fields(
|
||||
config,
|
||||
name="Second Group Configuration Name",
|
||||
description="Second Description of the group configuration."
|
||||
description="Second Description of the group configuration.",
|
||||
groups=["First Group", "Group C", "Group D"]
|
||||
)
|
||||
|
||||
def test_use_group_configuration(self):
|
||||
@@ -383,23 +420,30 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
Scenario: Ensure that the group configuration can be used by split_module correctly
|
||||
Given I have a course without group configurations
|
||||
When I create new group configuration
|
||||
And I set new name, save the group configuration
|
||||
And I set new name and add a new group, save the group configuration
|
||||
And I go to the unit page in Studio
|
||||
And I add new advanced module "Content Experiment"
|
||||
When I assign created group configuration to the module
|
||||
Then I see the module has correct groups
|
||||
And I go to the Group Configuration page in Studio
|
||||
And I edit the name of the group configuration
|
||||
And I edit the name of the group configuration, add new group and remove old one
|
||||
And I go to the unit page in Studio
|
||||
And I edit the unit
|
||||
Then I see the group configuration name is changed in `Group Configuration` dropdown
|
||||
And the group configuration name is changed on container page
|
||||
And I see the module has 2 active groups and one inactive
|
||||
And I see "Add missing groups" link exists
|
||||
When I click on "Add missing groups" link
|
||||
The I see the module has 3 active groups and one inactive
|
||||
"""
|
||||
self.page.visit()
|
||||
# Create new group configuration
|
||||
self.page.create()
|
||||
config = self.page.group_configurations()[0]
|
||||
config.name = "New Group Configuration Name"
|
||||
# Add new group
|
||||
config.add_group()
|
||||
config.groups[2].name = "New group"
|
||||
# Save the configuration
|
||||
config.save()
|
||||
|
||||
@@ -409,12 +453,16 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
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'], [])
|
||||
self.verify_groups(container, ['Group A', 'Group B', 'New group'], [])
|
||||
|
||||
self.page.visit()
|
||||
config = self.page.group_configurations()[0]
|
||||
config.edit()
|
||||
config.name = "Second Group Configuration Name"
|
||||
# Add new group
|
||||
config.add_group() # Group D
|
||||
# Remove Group A
|
||||
config.groups[0].remove()
|
||||
# Save the configuration
|
||||
config.save()
|
||||
|
||||
@@ -430,13 +478,20 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
"Second Group Configuration Name",
|
||||
container.get_xblock_information_message()
|
||||
)
|
||||
self.verify_groups(
|
||||
container, ['Group B', 'New group'], ['Group A'],
|
||||
verify_missing_groups_not_present=False
|
||||
)
|
||||
# Click the add button and verify that the groups were added on the page
|
||||
container.add_missing_groups()
|
||||
self.verify_groups(container, ['Group B', 'New group', 'Group D'], ['Group A'])
|
||||
|
||||
def test_can_cancel_creation_of_group_configuration(self):
|
||||
"""
|
||||
Scenario: Ensure that creation of the group configuration can be canceled correctly.
|
||||
Given I have a course without group configurations
|
||||
When I click button 'Create new Group Configuration'
|
||||
And I set new name and description
|
||||
And I set new name and description, add 1 additional group
|
||||
And I click button 'Cancel'
|
||||
Then I see that there is no new group configurations in the course
|
||||
"""
|
||||
@@ -449,6 +504,8 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
config = self.page.group_configurations()[0]
|
||||
config.name = "Name of the Group Configuration"
|
||||
config.description = "Description of the group configuration."
|
||||
# Add new group
|
||||
config.add_group() # Group C
|
||||
# Cancel the configuration
|
||||
config.cancel()
|
||||
|
||||
@@ -459,7 +516,7 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
Scenario: Ensure that editing of the group configuration can be canceled correctly.
|
||||
Given I have a course with group configuration
|
||||
When I go to the edit mode of the group configuration
|
||||
And I set new name and description
|
||||
And I set new name and description, add 2 additional groups
|
||||
And I click button 'Cancel'
|
||||
Then I see that new changes were discarded
|
||||
"""
|
||||
@@ -478,6 +535,9 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
|
||||
config.name = "New Group Configuration Name"
|
||||
config.description = "New Description of the group configuration."
|
||||
# Add 2 new groups
|
||||
config.add_group() # Group C
|
||||
config.add_group() # Group D
|
||||
# Cancel the configuration
|
||||
config.cancel()
|
||||
|
||||
@@ -495,27 +555,39 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
And I create new group configuration with 2 default groups
|
||||
When I set only description and try to save
|
||||
Then I see error message "Group Configuration name is required"
|
||||
When I set new name and try to save
|
||||
When I set a name
|
||||
And I delete the name of one of the groups and try to save
|
||||
Then I see error message "All groups must have a name"
|
||||
When I delete the group without name and try to save
|
||||
Then I see error message "Please add at least two groups"
|
||||
When I add new group and try to save
|
||||
Then I see the group configuration is saved successfully
|
||||
"""
|
||||
self.page.visit()
|
||||
def try_to_save_and_verify_error_message(message):
|
||||
# Try to save
|
||||
config.save()
|
||||
# Verify that configuration is still in editing mode
|
||||
self.assertEqual(config.mode, 'edit')
|
||||
# Verify error message
|
||||
self.assertEqual(message, config.validation_message)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
try_to_save_and_verify_error_message("Group Configuration name is required")
|
||||
|
||||
# Set required field
|
||||
config.name = "Name of the Group Configuration"
|
||||
config.groups[1].name = ''
|
||||
try_to_save_and_verify_error_message("All groups must have a name")
|
||||
config.groups[1].remove()
|
||||
try_to_save_and_verify_error_message("There must be at least two groups")
|
||||
config.add_group()
|
||||
|
||||
# Save the configuration
|
||||
config.save()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user