BLD-1121: Create new group configurations page.
This commit is contained in:
@@ -13,7 +13,7 @@ from django.conf import settings
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.core.exceptions import PermissionDenied
|
||||
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 edxmako.shortcuts import render_to_response
|
||||
|
||||
@@ -21,6 +21,7 @@ from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.tabs import PDFTextbookTabs
|
||||
from xmodule.partitions.partitions import UserPartition, Group
|
||||
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
|
||||
from opaque_keys import InvalidKeyError
|
||||
@@ -67,7 +68,7 @@ __all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler'
|
||||
'grading_handler',
|
||||
'advanced_settings_handler',
|
||||
'textbooks_list_handler', 'textbooks_detail_handler',
|
||||
'group_configurations_list_handler']
|
||||
'group_configurations_list_handler', 'group_configurations_detail_handler']
|
||||
|
||||
|
||||
class AccessListFallback(Exception):
|
||||
@@ -855,7 +856,56 @@ def textbooks_detail_handler(request, course_key_string, textbook_id):
|
||||
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
|
||||
@ensure_csrf_cookie
|
||||
def group_configurations_list_handler(request, course_key_string):
|
||||
@@ -864,17 +914,56 @@ def group_configurations_list_handler(request, course_key_string):
|
||||
|
||||
GET
|
||||
html: return Group Configurations list page (Backbone application)
|
||||
POST
|
||||
json: create new group configuration
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
course = _get_course_module(course_key, request.user)
|
||||
group_configuration_url = reverse_course_url('group_configurations_list_handler', course_key)
|
||||
splite_test_enabled = SPLIT_TEST_COMPONENT_TYPE in course.advanced_modules
|
||||
store = modulestore()
|
||||
|
||||
return render_to_response('group_configurations.html', {
|
||||
'context_course': course,
|
||||
'group_configuration_url': group_configuration_url,
|
||||
'configurations': [u.to_json() for u in course.user_partitions] if splite_test_enabled else None,
|
||||
})
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
|
||||
group_configuration_url = reverse_course_url('group_configurations_list_handler', course_key)
|
||||
split_test_enabled = SPLIT_TEST_COMPONENT_TYPE in course.advanced_modules
|
||||
|
||||
return render_to_response('group_configurations.html', {
|
||||
'context_course': course,
|
||||
'group_configuration_url': group_configuration_url,
|
||||
'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):
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
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)
|
||||
@@ -1,17 +1,18 @@
|
||||
define([
|
||||
'backbone', 'underscore', 'gettext', 'js/models/group',
|
||||
'backbone', 'underscore', 'underscore.string', 'gettext', 'js/models/group',
|
||||
'js/collections/group', 'backbone.associations', 'coffee/src/main'
|
||||
],
|
||||
function(Backbone, _, gettext, GroupModel, GroupCollection) {
|
||||
function(Backbone, _, str, gettext, GroupModel, GroupCollection) {
|
||||
'use strict';
|
||||
_.str = str;
|
||||
var GroupConfiguration = Backbone.AssociatedModel.extend({
|
||||
defaults: function() {
|
||||
return {
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
groups: new GroupCollection([{}, {}]),
|
||||
showGroups: false
|
||||
groups: new GroupCollection([]),
|
||||
showGroups: false,
|
||||
editing: false
|
||||
};
|
||||
},
|
||||
|
||||
@@ -55,32 +56,12 @@ function(Backbone, _, gettext, GroupModel, GroupCollection) {
|
||||
},
|
||||
|
||||
validate: function(attrs) {
|
||||
if (!attrs.name) {
|
||||
if (!_.str.trim(attrs.name)) {
|
||||
return {
|
||||
message: gettext('Group Configuration name is required'),
|
||||
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;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
define([
|
||||
'backbone', 'js/models/group_configuration',
|
||||
'js/collections/group_configuration', 'js/models/group',
|
||||
'js/collections/group', 'coffee/src/main'
|
||||
'backbone', 'coffee/src/main', 'js/models/group_configuration',
|
||||
'js/models/group', 'js/collections/group'
|
||||
], function(
|
||||
Backbone, GroupConfiguration, GroupConfigurationSet, Group, GroupSet, main
|
||||
Backbone, main, GroupConfigurationModel, GroupModel, GroupCollection
|
||||
) {
|
||||
'use strict';
|
||||
beforeEach(function() {
|
||||
@@ -14,10 +13,10 @@ define([
|
||||
});
|
||||
});
|
||||
|
||||
describe('GroupConfiguration model', function() {
|
||||
describe('GroupConfigurationModel', function() {
|
||||
beforeEach(function() {
|
||||
main();
|
||||
this.model = new GroupConfiguration();
|
||||
this.model = new GroupConfigurationModel();
|
||||
});
|
||||
|
||||
describe('Basic', function() {
|
||||
@@ -33,16 +32,10 @@ define([
|
||||
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');
|
||||
|
||||
expect(groups).toBeInstanceOf(GroupSet);
|
||||
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(groups).toBeInstanceOf(GroupCollection);
|
||||
expect(this.model.isEmpty()).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -53,21 +46,23 @@ define([
|
||||
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() {
|
||||
this.model.set('name', 'foobar');
|
||||
|
||||
expect(this.model.isDirty()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not be dirty after calling setOriginalAttributes', function() {
|
||||
this.model.set('name', 'foobar');
|
||||
this.model.setOriginalAttributes();
|
||||
describe('should not be dirty', function () {
|
||||
it('by default', function() {
|
||||
expect(this.model.isDirty()).toBeFalsy();
|
||||
});
|
||||
|
||||
expect(this.model.isDirty()).toBeFalsy();
|
||||
it('after calling setOriginalAttributes', function() {
|
||||
this.model.set('name', 'foobar');
|
||||
this.model.setOriginalAttributes();
|
||||
|
||||
expect(this.model.isDirty()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,7 +91,7 @@ define([
|
||||
it('should match server model to client model', function() {
|
||||
var serverModelSpec = {
|
||||
'id': 10,
|
||||
'name': 'My GroupConfiguration',
|
||||
'name': 'My Group Configuration',
|
||||
'description': 'Some description',
|
||||
'groups': [
|
||||
{
|
||||
@@ -108,9 +103,10 @@ define([
|
||||
},
|
||||
clientModelSpec = {
|
||||
'id': 10,
|
||||
'name': 'My GroupConfiguration',
|
||||
'name': 'My Group Configuration',
|
||||
'description': 'Some description',
|
||||
'showGroups': false,
|
||||
'editing': false,
|
||||
'groups': [
|
||||
{
|
||||
'name': 'Group 1'
|
||||
@@ -119,7 +115,7 @@ define([
|
||||
}
|
||||
]
|
||||
},
|
||||
model = new GroupConfiguration(serverModelSpec);
|
||||
model = new GroupConfigurationModel(serverModelSpec);
|
||||
|
||||
expect(deepAttributes(model)).toEqual(clientModelSpec);
|
||||
expect(model.toJSON()).toEqual(serverModelSpec);
|
||||
@@ -128,55 +124,22 @@ define([
|
||||
|
||||
describe('Validation', function() {
|
||||
it('requires a name', function() {
|
||||
var model = new GroupConfiguration({ 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]);
|
||||
var model = new GroupConfigurationModel({ name: '' });
|
||||
|
||||
expect(model.isValid()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can pass validation', function() {
|
||||
var group = new Group(),
|
||||
model = new GroupConfiguration({ name: 'foo' });
|
||||
|
||||
group.isValid = function() { return true; };
|
||||
model.get('groups').reset([group]);
|
||||
var model = new GroupConfigurationModel({ name: 'foo' });
|
||||
|
||||
expect(model.isValid()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Group model', function() {
|
||||
describe('GroupModel', function() {
|
||||
beforeEach(function() {
|
||||
this.model = new Group();
|
||||
this.model = new GroupModel();
|
||||
});
|
||||
|
||||
describe('Basic', function() {
|
||||
@@ -191,22 +154,22 @@ define([
|
||||
|
||||
describe('Validation', function() {
|
||||
it('requires a name', function() {
|
||||
var model = new Group({ name: '' });
|
||||
var model = new GroupModel({ name: '' });
|
||||
|
||||
expect(model.isValid()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can pass validation', function() {
|
||||
var model = new Group({ name: 'a' });
|
||||
var model = new GroupModel({ name: 'a' });
|
||||
|
||||
expect(model.isValid()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Group collection', function() {
|
||||
describe('GroupCollection', function() {
|
||||
beforeEach(function() {
|
||||
this.collection = new GroupSet();
|
||||
this.collection = new GroupCollection();
|
||||
});
|
||||
|
||||
it('is empty by default', function() {
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
define([
|
||||
'js/models/group_configuration', 'js/models/course',
|
||||
'js/collections/group_configuration', 'js/views/group_configuration_details',
|
||||
'js/views/group_configurations_list', 'jasmine-stealth'
|
||||
'js/models/course', 'js/models/group_configuration',
|
||||
'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'
|
||||
], function(
|
||||
GroupConfigurationModel, Course, GroupConfigurationSet,
|
||||
GroupConfigurationDetails, GroupConfigurationsList
|
||||
Course, GroupConfigurationModel, GroupConfigurationCollection,
|
||||
GroupConfigurationDetails, GroupConfigurationsList, GroupConfigurationEdit,
|
||||
GroupConfigurationItem, Notification, create_sinon, view_helpers
|
||||
) {
|
||||
'use strict';
|
||||
var SELECTORS = {
|
||||
detailsView: '.group-configuration-details',
|
||||
editView: '.group-configuration-edit',
|
||||
itemView: '.group-configurations-list-item',
|
||||
group: '.group',
|
||||
name: '.group-configuration-name',
|
||||
description: '.group-configuration-description',
|
||||
groupsCount: '.group-configuration-groups-count',
|
||||
groupsAllocation: '.group-allocation',
|
||||
errorMessage: '.group-configuration-edit-error',
|
||||
inputName: '.group-configuration-name-input',
|
||||
inputDescription: '.group-configuration-description-input'
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
window.course = new Course({
|
||||
id: '5',
|
||||
@@ -26,6 +45,20 @@ define([
|
||||
} else {
|
||||
return trimmedText.indexOf(text) !== -1;
|
||||
}
|
||||
},
|
||||
toBeCorrectValuesInInputs: function (values) {
|
||||
var expected = {
|
||||
name: this.actual.$(SELECTORS.inputName).val(),
|
||||
description: this.actual
|
||||
.$(SELECTORS.inputDescription).val()
|
||||
};
|
||||
|
||||
return _.isEqual(values, expected);
|
||||
},
|
||||
toBeCorrectValuesInModel: function (values) {
|
||||
return _.every(values, function (value, key) {
|
||||
return this.actual.get(key) === value;
|
||||
}.bind(this));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -35,13 +68,8 @@ define([
|
||||
});
|
||||
|
||||
describe('GroupConfigurationDetails', function() {
|
||||
var tpl = readFixtures('group-configuration-details.underscore');
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures($('<script>', {
|
||||
id: 'group-configuration-details-tpl',
|
||||
type: 'text/template'
|
||||
}).text(tpl));
|
||||
view_helpers.installTemplate('group-configuration-details', true);
|
||||
|
||||
this.model = new GroupConfigurationModel({
|
||||
name: 'Configuration',
|
||||
@@ -49,97 +77,221 @@ define([
|
||||
id: 0
|
||||
});
|
||||
|
||||
spyOn(this.model, 'destroy').andCallThrough();
|
||||
this.collection = new GroupConfigurationSet([ this.model ]);
|
||||
this.collection = new GroupConfigurationCollection([ this.model ]);
|
||||
this.view = new GroupConfigurationDetails({
|
||||
model: this.model
|
||||
});
|
||||
appendSetFixtures(this.view.render().el);
|
||||
});
|
||||
|
||||
describe('Basic', function() {
|
||||
it('should render properly', function() {
|
||||
this.view.render();
|
||||
it('should render properly', function() {
|
||||
expect(this.view.$el).toContainText('Configuration');
|
||||
expect(this.view.$el).toContainText('ID: 0');
|
||||
});
|
||||
|
||||
expect(this.view.$el).toContainText('Configuration');
|
||||
expect(this.view.$el).toContainText('ID: 0');
|
||||
it('should show groups appropriately', function() {
|
||||
this.model.get('groups').add([{}, {}, {}]);
|
||||
this.model.set('showGroups', false);
|
||||
this.view.$('.show-groups').click();
|
||||
|
||||
expect(this.model.get('showGroups')).toBeTruthy();
|
||||
expect(this.view.$(SELECTORS.group).length).toBe(3);
|
||||
expect(this.view.$(SELECTORS.groupsCount)).not.toExist();
|
||||
expect(this.view.$(SELECTORS.description))
|
||||
.toContainText('Configuration Description');
|
||||
expect(this.view.$(SELECTORS.groupsAllocation))
|
||||
.toContainText('33%');
|
||||
});
|
||||
|
||||
it('should hide groups appropriately', function() {
|
||||
this.model.get('groups').add([{}, {}, {}]);
|
||||
this.model.set('showGroups', true);
|
||||
this.view.$('.hide-groups').click();
|
||||
|
||||
expect(this.model.get('showGroups')).toBeFalsy();
|
||||
expect(this.view.$(SELECTORS.group)).not.toExist();
|
||||
expect(this.view.$(SELECTORS.groupsCount))
|
||||
.toContainText('Contains 3 groups');
|
||||
expect(this.view.$(SELECTORS.description)).not.toExist();
|
||||
expect(this.view.$(SELECTORS.groupsAllocation)).not.toExist();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GroupConfigurationEdit', function() {
|
||||
|
||||
var setValuesToInputs = function (view, values) {
|
||||
_.each(values, function (value, selector) {
|
||||
if (SELECTORS[selector]) {
|
||||
view.$(SELECTORS[selector]).val(value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
view_helpers.installViewTemplates();
|
||||
view_helpers.installTemplate('group-configuration-edit');
|
||||
|
||||
this.model = new GroupConfigurationModel({
|
||||
name: 'Configuration',
|
||||
description: 'Configuration Description',
|
||||
id: 0,
|
||||
editing: true
|
||||
});
|
||||
this.collection = new GroupConfigurationCollection([this.model]);
|
||||
this.collection.url = '/group_configurations';
|
||||
this.view = new GroupConfigurationEdit({
|
||||
model: this.model
|
||||
});
|
||||
appendSetFixtures(this.view.render().el);
|
||||
});
|
||||
|
||||
it('should render properly', function() {
|
||||
expect(this.view).toBeCorrectValuesInInputs({
|
||||
name: 'Configuration',
|
||||
description: 'Configuration Description'
|
||||
});
|
||||
});
|
||||
|
||||
it('should save properly', function() {
|
||||
var requests = create_sinon.requests(this),
|
||||
notificationSpy = view_helpers.createNotificationSpy();
|
||||
|
||||
setValuesToInputs(this.view, {
|
||||
inputName: 'New Configuration',
|
||||
inputDescription: 'New Description'
|
||||
});
|
||||
|
||||
it('should show groups appropriately', function() {
|
||||
this.model.get('groups').add([{}, {}, {}]);
|
||||
this.model.set('showGroups', false);
|
||||
this.view.render().$('.show-groups').click();
|
||||
this.view.$('form').submit();
|
||||
view_helpers.verifyNotificationShowing(notificationSpy, /Saving/);
|
||||
requests[0].respond(200);
|
||||
view_helpers.verifyNotificationHidden(notificationSpy);
|
||||
|
||||
expect(this.model.get('showGroups')).toBeTruthy();
|
||||
expect(this.view.$el.find('.group').length).toBe(5);
|
||||
expect(this.view.$el.find('.group-configuration-groups-count'))
|
||||
.not.toExist();
|
||||
expect(this.view.$el.find('.group-configuration-description'))
|
||||
.toContainText('Configuration Description');
|
||||
expect(this.view.$el.find('.group-allocation'))
|
||||
.toContainText('20%');
|
||||
expect(this.model).toBeCorrectValuesInModel({
|
||||
name: 'New Configuration',
|
||||
description: 'New Description'
|
||||
});
|
||||
expect(this.view.$el).not.toExist();
|
||||
});
|
||||
|
||||
it('should hide groups appropriately', function() {
|
||||
this.model.get('groups').add([{}, {}, {}]);
|
||||
this.model.set('showGroups', true);
|
||||
this.view.render().$('.hide-groups').click();
|
||||
it('does not hide saving message if failure', function() {
|
||||
var requests = create_sinon.requests(this),
|
||||
notificationSpy = view_helpers.createNotificationSpy();
|
||||
|
||||
expect(this.model.get('showGroups')).toBeFalsy();
|
||||
expect(this.view.$el.find('.group').length).toBe(0);
|
||||
expect(this.view.$el.find('.group-configuration-groups-count'))
|
||||
.toContainText('Contains 5 groups');
|
||||
expect(this.view.$el.find('.group-configuration-description'))
|
||||
.not.toExist();
|
||||
expect(this.view.$el.find('.group-allocation'))
|
||||
.not.toExist();
|
||||
setValuesToInputs(this.view, { inputName: 'New Configuration' });
|
||||
this.view.$('form').submit();
|
||||
view_helpers.verifyNotificationShowing(notificationSpy, /Saving/);
|
||||
create_sinon.respondWithError(requests);
|
||||
view_helpers.verifyNotificationShowing(notificationSpy, /Saving/);
|
||||
});
|
||||
|
||||
it('does not save on cancel', function() {
|
||||
setValuesToInputs(this.view, {
|
||||
inputName: 'New Configuration',
|
||||
inputDescription: 'New Description'
|
||||
});
|
||||
this.view.$('.action-cancel').click();
|
||||
expect(this.model).toBeCorrectValuesInModel({
|
||||
name: 'Configuration',
|
||||
description: 'Configuration Description'
|
||||
});
|
||||
// Model is still exist in the collection
|
||||
expect(this.collection.indexOf(this.model)).toBeGreaterThan(-1);
|
||||
expect(this.collection.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should be removed on cancel if it is a new item', function() {
|
||||
spyOn(this.model, 'isNew').andReturn(true);
|
||||
setValuesToInputs(this.view, {
|
||||
inputName: 'New Configuration',
|
||||
inputDescription: 'New Description'
|
||||
});
|
||||
this.view.$('.action-cancel').click();
|
||||
// Model is removed from the collection
|
||||
expect(this.collection.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should be possible to correct validation errors', function() {
|
||||
var requests = create_sinon.requests(this);
|
||||
|
||||
// Set incorrect value
|
||||
setValuesToInputs(this.view, { inputName: '' });
|
||||
// Try to save
|
||||
this.view.$('form').submit();
|
||||
// See error message
|
||||
expect(this.view.$(SELECTORS.errorMessage)).toContainText(
|
||||
'Group Configuration name is required'
|
||||
);
|
||||
// No request
|
||||
expect(requests.length).toBe(0);
|
||||
// Set correct value
|
||||
setValuesToInputs(this.view, { inputName: 'New Configuration' });
|
||||
// Try to save
|
||||
this.view.$('form').submit();
|
||||
requests[0].respond(200);
|
||||
// Model is updated
|
||||
expect(this.model).toBeCorrectValuesInModel({
|
||||
name: 'New Configuration'
|
||||
});
|
||||
// Error message disappear
|
||||
expect(this.view.$(SELECTORS.errorMessage)).not.toExist();
|
||||
expect(requests.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GroupConfigurationsList', function() {
|
||||
var noGroupConfigurationsTpl = readFixtures(
|
||||
'no-group-configurations.underscore'
|
||||
);
|
||||
|
||||
beforeEach(function() {
|
||||
var showEl = $('<li>');
|
||||
view_helpers.installTemplate('no-group-configurations', true);
|
||||
|
||||
setFixtures($('<script>', {
|
||||
id: 'no-group-configurations-tpl',
|
||||
type: 'text/template'
|
||||
}).text(noGroupConfigurationsTpl));
|
||||
|
||||
this.showSpies = spyOnConstructor(
|
||||
window, 'GroupConfigurationDetails', [ 'render' ]
|
||||
);
|
||||
this.showSpies.render.andReturn(this.showSpies);
|
||||
this.showSpies.$el = showEl;
|
||||
this.showSpies.el = showEl.get(0);
|
||||
this.collection = new GroupConfigurationSet();
|
||||
this.collection = new GroupConfigurationCollection();
|
||||
this.view = new GroupConfigurationsList({
|
||||
collection: this.collection
|
||||
});
|
||||
this.view.render();
|
||||
appendSetFixtures(this.view.render().el);
|
||||
});
|
||||
|
||||
var message = 'should render the empty template if there are no group ' +
|
||||
'configurations';
|
||||
it(message, function() {
|
||||
expect(this.view.$el).toContainText(
|
||||
'You haven\'t created any group configurations yet.'
|
||||
);
|
||||
expect(this.view.$el).not.toContain('.new-button');
|
||||
expect(this.showSpies.constructor).not.toHaveBeenCalled();
|
||||
describe('empty template', function () {
|
||||
it('should be rendered if no group configurations', function() {
|
||||
expect(this.view.$el).toContainText(
|
||||
'You haven\'t created any group configurations yet.'
|
||||
);
|
||||
expect(this.view.$el).toContain('.new-button');
|
||||
expect(this.view.$(SELECTORS.itemView)).not.toExist();
|
||||
});
|
||||
|
||||
it('should disappear if group configuration is added', function() {
|
||||
var emptyMessage = 'You haven\'t created any group ' +
|
||||
'configurations yet.';
|
||||
|
||||
expect(this.view.$el).toContainText(emptyMessage);
|
||||
expect(this.view.$(SELECTORS.itemView)).not.toExist();
|
||||
this.collection.add([{}]);
|
||||
expect(this.view.$el).not.toContainText(emptyMessage);
|
||||
expect(this.view.$(SELECTORS.itemView)).toExist();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GroupConfigurationItem', function() {
|
||||
beforeEach(function() {
|
||||
this.model = new GroupConfigurationModel({ });
|
||||
this.collection = new GroupConfigurationCollection([ this.model ]);
|
||||
this.view = new GroupConfigurationItem({
|
||||
model: this.model
|
||||
});
|
||||
appendSetFixtures(this.view.render().el);
|
||||
});
|
||||
|
||||
it('should render GroupConfigurationDetails views by default', function() {
|
||||
this.collection.add([{}, {}, {}]);
|
||||
this.view.render();
|
||||
|
||||
expect(this.view.$el).not.toContainText(
|
||||
'You haven\'t created any group configurations yet.'
|
||||
);
|
||||
expect(this.view.$el.find('.group-configuration').length).toBe(3);
|
||||
it('should render properly', function() {
|
||||
// Details view by default
|
||||
expect(this.view.$(SELECTORS.detailsView)).toExist();
|
||||
this.model.set('editing', true);
|
||||
expect(this.view.$(SELECTORS.editView)).toExist();
|
||||
expect(this.view.$(SELECTORS.detailsView)).not.toExist();
|
||||
this.model.set('editing', false);
|
||||
expect(this.view.$(SELECTORS.detailsView)).toExist();
|
||||
expect(this.view.$(SELECTORS.editView)).not.toExist();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -9,19 +9,32 @@ define([
|
||||
),
|
||||
noGroupConfigurationsTpl = readFixtures(
|
||||
'no-group-configurations.underscore'
|
||||
), view;
|
||||
),
|
||||
groupConfigurationEditTpl = readFixtures(
|
||||
'group-configuration-edit.underscore'
|
||||
);
|
||||
|
||||
var initializePage = function (disableSpy) {
|
||||
view = new GroupConfigurationsPage({
|
||||
el: $('.content-primary'),
|
||||
var view = new GroupConfigurationsPage({
|
||||
el: $('#content'),
|
||||
collection: new GroupConfigurationCollection({
|
||||
name: 'Configuration 1'
|
||||
})
|
||||
});
|
||||
|
||||
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 () {
|
||||
@@ -29,12 +42,18 @@ define([
|
||||
id: 'no-group-configurations-tpl',
|
||||
type: 'text/template'
|
||||
}).text(noGroupConfigurationsTpl));
|
||||
|
||||
appendSetFixtures($('<script>', {
|
||||
id: 'group-configuration-edit-tpl',
|
||||
type: 'text/template'
|
||||
}).text(groupConfigurationEditTpl));
|
||||
|
||||
appendSetFixtures(mockGroupConfigurationsPage);
|
||||
});
|
||||
|
||||
describe('Initial display', function() {
|
||||
it('can render itself', function() {
|
||||
initializePage();
|
||||
var view = initializePage();
|
||||
expect(view.$('.ui-loading')).toBeVisible();
|
||||
view.render();
|
||||
expect(view.$('.no-group-configurations-content')).toBeTruthy();
|
||||
@@ -45,9 +64,9 @@ define([
|
||||
describe('on page close/change', function() {
|
||||
it('I see notification message if the model is changed',
|
||||
function() {
|
||||
var message;
|
||||
var view = initializePage(true),
|
||||
message;
|
||||
|
||||
initializePage(true);
|
||||
view.render();
|
||||
message = view.onBeforeUnload();
|
||||
expect(message).toBeUndefined();
|
||||
@@ -56,16 +75,23 @@ define([
|
||||
it('I do not see notification message if the model is not changed',
|
||||
function() {
|
||||
var expectedMessage = [
|
||||
'You have unsaved changes. Do you really want to ',
|
||||
'leave this page?'
|
||||
].join(''), message;
|
||||
'You have unsaved changes. Do you really want to ',
|
||||
'leave this page?'
|
||||
].join(''),
|
||||
view = renderPage(),
|
||||
message;
|
||||
|
||||
initializePage();
|
||||
view.render();
|
||||
view.collection.at(0).set('name', 'Configuration 2');
|
||||
message = view.onBeforeUnload();
|
||||
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([
|
||||
function(BaseView, _, gettext) {
|
||||
'use strict';
|
||||
var GroupConfigurationDetails = BaseView.extend({
|
||||
tagName: 'section',
|
||||
className: 'group-configuration',
|
||||
tagName: 'div',
|
||||
events: {
|
||||
'click .show-groups': 'showGroups',
|
||||
'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() {
|
||||
this.template = _.template(
|
||||
$('#group-configuration-details-tpl').text()
|
||||
@@ -41,7 +49,10 @@ function(BaseView, _, gettext) {
|
||||
getGroupsCountTitle: function () {
|
||||
var count = this.model.get('groups').length,
|
||||
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',
|
||||
count
|
||||
);
|
||||
|
||||
118
cms/static/js/views/group_configuration_edit.js
Normal file
118
cms/static/js/views/group_configuration_edit.js
Normal file
@@ -0,0 +1,118 @@
|
||||
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') + '…',
|
||||
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;
|
||||
});
|
||||
53
cms/static/js/views/group_configuration_item.js
Normal file
53
cms/static/js/views/group_configuration_item.js
Normal file
@@ -0,0 +1,53 @@
|
||||
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;
|
||||
});
|
||||
@@ -1,25 +1,31 @@
|
||||
define(['js/views/baseview', 'jquery', 'js/views/group_configuration_details'],
|
||||
function(BaseView, $, GroupConfigurationDetailsView) {
|
||||
define([
|
||||
'js/views/baseview', 'jquery', 'js/views/group_configuration_item'
|
||||
], function(
|
||||
BaseView, $, GroupConfigurationItemView
|
||||
) {
|
||||
'use strict';
|
||||
var GroupConfigurationsList = BaseView.extend({
|
||||
tagName: 'div',
|
||||
className: 'group-configurations-list',
|
||||
events: { },
|
||||
events: {
|
||||
'click .new-button': 'addOne'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.emptyTemplate = this.loadTemplate('no-group-configurations');
|
||||
this.listenTo(this.collection, 'all', this.render);
|
||||
this.listenTo(this.collection, 'add', this.addNewItemView);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var configurations = this.collection;
|
||||
|
||||
if(configurations.length === 0) {
|
||||
this.$el.html(this.emptyTemplate());
|
||||
} else {
|
||||
var frag = document.createDocumentFragment();
|
||||
|
||||
configurations.each(function(configuration) {
|
||||
var view = new GroupConfigurationDetailsView({
|
||||
var view = new GroupConfigurationItemView({
|
||||
model: configuration
|
||||
});
|
||||
|
||||
@@ -29,6 +35,27 @@ function(BaseView, $, GroupConfigurationDetailsView) {
|
||||
this.$el.html([frag]);
|
||||
}
|
||||
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([
|
||||
'jquery', 'underscore', 'gettext', 'js/views/baseview',
|
||||
'js/views/group_configurations_list'
|
||||
],
|
||||
function ($, _, gettext, BaseView, ConfigurationsListView) {
|
||||
function ($, _, gettext, BaseView, GroupConfigurationsList) {
|
||||
'use strict';
|
||||
var GroupConfigurationsPage = BaseView.extend({
|
||||
initialize: function() {
|
||||
BaseView.prototype.initialize.call(this);
|
||||
this.listView = new ConfigurationsListView({
|
||||
this.listView = new GroupConfigurationsList({
|
||||
collection: this.collection
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.hideLoadingIndicator();
|
||||
this.$el.append(this.listView.render().el);
|
||||
this.addGlobalActions();
|
||||
this.$('.content-primary').append(this.listView.render().el);
|
||||
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));
|
||||
},
|
||||
|
||||
|
||||
@@ -27,11 +27,21 @@
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
.new-button {
|
||||
@include font-size(14);
|
||||
margin-left: $baseline;
|
||||
|
||||
[class^="icon-"] {
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
.group-configuration {
|
||||
@extend %ui-window;
|
||||
position: relative;
|
||||
outline: none;
|
||||
|
||||
.view-group-configuration {
|
||||
.group-configuration-details {
|
||||
padding: $baseline ($baseline*1.5);
|
||||
|
||||
.group-configuration-header {
|
||||
@@ -60,6 +70,7 @@
|
||||
}
|
||||
|
||||
.group-configuration-info {
|
||||
@extend %t-copy-sub1;
|
||||
color: $gray-l1;
|
||||
margin-left: $baseline;
|
||||
|
||||
@@ -152,6 +163,182 @@
|
||||
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 {
|
||||
|
||||
@@ -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", "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">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
@@ -20,14 +20,16 @@
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
require(["!domReady", "js/collections/group_configuration", "js/views/pages/group_configurations"],
|
||||
function(doc, GroupConfigurationCollection, GroupConfigurationsPage, xmoduleLoader) {
|
||||
require(["domReady!", "js/collections/group_configuration", "js/views/pages/group_configurations"],
|
||||
function(doc, GroupConfigurationCollection, GroupConfigurationsPage) {
|
||||
% if configurations is not None:
|
||||
var view = new GroupConfigurationsPage({
|
||||
el: $('.content-primary'),
|
||||
collection: new GroupConfigurationCollection(${json.dumps(configurations)}, { url: "${group_configuration_url}" })
|
||||
});
|
||||
view.render();
|
||||
var collection = new GroupConfigurationCollection(${json.dumps(configurations)});
|
||||
|
||||
collection.url = "${group_configuration_url}";
|
||||
new GroupConfigurationsPage({
|
||||
el: $('#content'),
|
||||
collection: collection
|
||||
}).render();
|
||||
% endif
|
||||
});
|
||||
</script>
|
||||
@@ -40,6 +42,14 @@ function(doc, GroupConfigurationCollection, GroupConfigurationsPage, xmoduleLoad
|
||||
<small class="subtitle">${_("Settings")}</small>
|
||||
<span class="sr">> </span>${_("Group Configurations")}
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,42 +1,40 @@
|
||||
<div class="view-group-configuration view-group-configuration-<%= index %>">
|
||||
<div class="wrapper-group-configuration">
|
||||
<header class="group-configuration-header">
|
||||
<h3 class="group-configuration-title">
|
||||
<a href="#" class="group-toggle <% if(showGroups){ print('hide'); } else { print('show'); } %>-groups">
|
||||
<i class="ui-toggle-expansion icon-caret-<% if(showGroups){ print('down'); } else { print('right'); } %>"></i>
|
||||
<%= name %>
|
||||
</a>
|
||||
</h3>
|
||||
</header>
|
||||
<div class="wrapper-group-configuration">
|
||||
<header class="group-configuration-header">
|
||||
<h3 class="group-configuration-title">
|
||||
<a href="#" class="group-toggle <% if(showGroups){ print('hide'); } else { print('show'); } %>-groups">
|
||||
<i class="ui-toggle-expansion icon-caret-<% if(showGroups){ print('down'); } else { print('right'); } %>"></i>
|
||||
<%= name %>
|
||||
</a>
|
||||
</h3>
|
||||
</header>
|
||||
|
||||
<ol class="group-configuration-info group-configuration-info-<% if(showGroups){ print('block'); } else { print('inline'); } %>">
|
||||
<% if (_.isNumber(id)) { %>
|
||||
<li class="group-configuration-id"
|
||||
><span class="group-configuration-label"><%= gettext('ID') %>: </span
|
||||
><span class="group-configuration-value"><%= id %></span
|
||||
<ol class="group-configuration-info group-configuration-info-<% if(showGroups){ print('block'); } else { print('inline'); } %>">
|
||||
<% if (!_.isUndefined(id)) { %>
|
||||
<li class="group-configuration-id"
|
||||
><span class="group-configuration-label"><%= gettext('ID') %>: </span
|
||||
><span class="group-configuration-value"><%= id %></span
|
||||
></li>
|
||||
<% } %>
|
||||
<% if (showGroups) { %>
|
||||
<li class="group-configuration-description">
|
||||
<%= description %>
|
||||
</li>
|
||||
<% } else { %>
|
||||
<li class="group-configuration-groups-count">
|
||||
<%= groupsCountMessage %>
|
||||
</li>
|
||||
<% } %>
|
||||
</ol>
|
||||
|
||||
<% if(showGroups) { %>
|
||||
<% allocation = Math.floor(100 / groups.length) %>
|
||||
<ol class="groups groups-<%= index %>">
|
||||
<% groups.each(function(group, groupIndex) { %>
|
||||
<li class="group group-<%= groupIndex %>"
|
||||
><span class="group-name"><%= group.get('name') %></span
|
||||
><span class="group-allocation"><%= allocation %>%</span
|
||||
></li>
|
||||
<% } %>
|
||||
<% if (showGroups) { %>
|
||||
<li class="group-configuration-description">
|
||||
<%= description %>
|
||||
</li>
|
||||
<% } else { %>
|
||||
<li class="group-configuration-groups-count">
|
||||
<%= groupsCountMessage %>
|
||||
</li>
|
||||
<% } %>
|
||||
<% }) %>
|
||||
</ol>
|
||||
|
||||
<% if(showGroups) { %>
|
||||
<% allocation = Math.floor(100 / groups.length) %>
|
||||
<ol class="groups groups-<%= index %>">
|
||||
<% groups.each(function(group, groupIndex) { %>
|
||||
<li class="group group-<%= groupIndex %>"
|
||||
><span class="group-name"><%= group.get('name') %></span
|
||||
><span class="group-allocation"><%= allocation %>%</span
|
||||
></li>
|
||||
<% }) %>
|
||||
</ol>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
26
cms/templates/js/group-configuration-edit.underscore
Normal file
26
cms/templates/js/group-configuration-edit.underscore
Normal file
@@ -0,0 +1,26 @@
|
||||
<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>
|
||||
@@ -1,11 +1,18 @@
|
||||
<div id="content">
|
||||
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions has-subtitle">
|
||||
<h1 class="page-header">
|
||||
<small class="subtitle">${_("Settings")}</small>
|
||||
<span class="sr">> </span>${_("Group Configurations")}
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<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>
|
||||
|
||||
@@ -93,8 +93,11 @@ urlpatterns += patterns(
|
||||
)
|
||||
|
||||
if settings.FEATURES.get('ENABLE_GROUP_CONFIGURATIONS'):
|
||||
urlpatterns += (url(r'^group_configurations/(?P<course_key_string>[^/]+)$',
|
||||
'contentstore.views.group_configurations_list_handler'),)
|
||||
urlpatterns += patterns('contentstore.views',
|
||||
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 = {
|
||||
'domain': 'djangojs',
|
||||
|
||||
@@ -19,9 +19,15 @@ class GroupConfigurationsPage(CoursePage):
|
||||
"""
|
||||
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)))]
|
||||
|
||||
def create(self):
|
||||
"""
|
||||
Creates new group configuration.
|
||||
"""
|
||||
self.q(css=".new-button").first.click()
|
||||
|
||||
|
||||
class GroupConfiguration(object):
|
||||
"""
|
||||
@@ -30,7 +36,7 @@ class GroupConfiguration(object):
|
||||
|
||||
def __init__(self, page, index):
|
||||
self.page = page
|
||||
self.SELECTOR = '.view-group-configuration-{}'.format(index)
|
||||
self.SELECTOR = '.group-configurations-list-item-{}'.format(index)
|
||||
self.index = index
|
||||
|
||||
def get_selector(self, css=''):
|
||||
@@ -49,6 +55,31 @@ class GroupConfiguration(object):
|
||||
css = 'a.group-toggle'
|
||||
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
|
||||
def id(self):
|
||||
"""
|
||||
@@ -57,6 +88,14 @@ class GroupConfiguration(object):
|
||||
css = '.group-configuration-id .group-configuration-value'
|
||||
return self.find_css(css).first.text[0]
|
||||
|
||||
@property
|
||||
def validation_message(self):
|
||||
"""
|
||||
Returns validation message.
|
||||
"""
|
||||
css = '.message-status.error'
|
||||
return self.find_css(css).first.text[0]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
@@ -65,6 +104,14 @@ class GroupConfiguration(object):
|
||||
css = '.group-configuration-title'
|
||||
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
|
||||
def description(self):
|
||||
"""
|
||||
@@ -73,6 +120,14 @@ class GroupConfiguration(object):
|
||||
css = '.group-configuration-description'
|
||||
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
|
||||
def groups(self):
|
||||
"""
|
||||
|
||||
@@ -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,6 +93,30 @@ 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.
|
||||
|
||||
@@ -57,29 +57,7 @@ class SplitTest(ContainerBase):
|
||||
self.user = course_fix.user
|
||||
|
||||
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:])
|
||||
|
||||
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)
|
||||
|
||||
@@ -225,33 +203,33 @@ class SettingsMenuTest(UniqueCourseTest):
|
||||
|
||||
|
||||
@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
|
||||
added configurations in Studio
|
||||
"""
|
||||
__test__ = True
|
||||
|
||||
def setUp(self):
|
||||
super(GroupConfigurationsTest, self).setUp()
|
||||
|
||||
def setup_fixtures(self):
|
||||
course_fix = CourseFixture(**self.course_info)
|
||||
course_fix.add_advanced_settings({
|
||||
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()
|
||||
|
||||
self.course_fix = course_fix
|
||||
|
||||
course_fix.install()
|
||||
self.course_fix = course_fix
|
||||
self.user = course_fix.user
|
||||
|
||||
self.auth_page = AutoAuthPage(
|
||||
self.browser,
|
||||
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.browser,
|
||||
self.course_info['org'],
|
||||
@@ -292,6 +270,7 @@ class GroupConfigurationsTest(UniqueCourseTest):
|
||||
config = self.page.group_configurations()[0]
|
||||
self.assertIn("Name of the Group Configuration", config.name)
|
||||
self.assertEqual(config.id, '0')
|
||||
# Expand the configuration
|
||||
config.toggle()
|
||||
self.assertIn("Description of the group configuration.", config.description)
|
||||
self.assertEqual(len(config.groups), 2)
|
||||
@@ -302,8 +281,111 @@ class GroupConfigurationsTest(UniqueCourseTest):
|
||||
config = self.page.group_configurations()[1]
|
||||
self.assertIn("Name of second Group Configuration", config.name)
|
||||
self.assertEqual(len(config.groups), 0) # no groups when the partition is collapsed
|
||||
# Expand the configuration
|
||||
config.toggle()
|
||||
self.assertEqual(len(config.groups), 3)
|
||||
|
||||
self.assertEqual("Beta", config.groups[1].name)
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user