BLD-1049: Remove Group Configurations.

This commit is contained in:
polesye
2014-07-24 11:30:45 +03:00
parent 9c52e5d74d
commit 27013e74f4
18 changed files with 661 additions and 290 deletions

View File

@@ -1,5 +1,5 @@
define([
'js/models/course', 'js/models/group_configuration',
'underscore', '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',
@@ -8,7 +8,7 @@ define([
'js/views/feedback_notification', 'js/spec_helpers/create_sinon',
'js/spec_helpers/edit_helpers', 'jasmine-stealth'
], function(
Course, GroupConfigurationModel, GroupConfigurationCollection,
_, Course, GroupConfigurationModel, GroupConfigurationCollection,
GroupConfigurationDetails, GroupConfigurationsList, GroupConfigurationEdit,
GroupConfigurationItem, GroupModel, GroupCollection, GroupEdit,
Notification, create_sinon, view_helpers
@@ -33,7 +33,8 @@ define([
usageText: '.group-configuration-usage-text',
usageTextAnchor: '.group-configuration-usage-text > a',
usageUnit: '.group-configuration-usage-unit',
usageUnitAnchor: '.group-configuration-usage-unit > a'
usageUnitAnchor: '.group-configuration-usage-unit > a',
note: '.wrapper-delete-button'
};
beforeEach(function() {
@@ -105,6 +106,7 @@ define([
it('should render properly', function() {
expect(this.view.$el).toContainText('Configuration');
expect(this.view.$el).toContainText('ID: 0');
expect(this.view.$('.delete')).toExist();
});
it('should show groups appropriately', function() {
@@ -171,6 +173,10 @@ define([
usageUnitAnchors = this.view.$(SELECTORS.usageUnitAnchor);
expect(this.view.$(SELECTORS.note)).toHaveAttr(
'data-tooltip', 'Cannot delete when in use by an experiment'
);
expect(this.view.$('.delete')).toHaveClass('is-disabled');
expect(this.view.$(SELECTORS.usageCount)).not.toExist();
expect(this.view.$(SELECTORS.usageText))
.toContainText('This Group Configuration is used in:');
@@ -183,15 +189,17 @@ define([
});
it('should hide non-empty usage appropriately', function() {
this.model.set('usage',
[
{'label': 'label1', 'url': 'url1'},
{'label': 'label2', 'url': 'url2'}
]
);
this.model.set('usage', [
{'label': 'label1', 'url': 'url1'},
{'label': 'label2', 'url': 'url2'}
]);
this.model.set('showGroups', true);
this.view.$('.hide-groups').click();
expect(this.view.$(SELECTORS.note)).toHaveAttr(
'data-tooltip', 'Cannot delete when in use by an experiment'
);
expect(this.view.$('.delete')).toHaveClass('is-disabled');
expect(this.view.$(SELECTORS.usageText)).not.toExist();
expect(this.view.$(SELECTORS.usageUnit)).not.toExist();
expect(this.view.$(SELECTORS.usageCount))
@@ -234,6 +242,7 @@ define([
name: 'Configuration',
description: 'Configuration Description'
});
expect(this.view.$('.delete')).toExist();
});
it ('should allow you to create new groups', function() {
@@ -372,7 +381,7 @@ define([
});
});
it('groups have correct default names and placeholders', function () {
it('groups have correct default names', function () {
var group1 = new GroupModel({ name: 'Group A' }),
group2 = new GroupModel({ name: 'Group B' }),
collection = this.model.get('groups');
@@ -400,12 +409,24 @@ define([
'Group A', 'Group C', 'Group D', 'Group E', 'Group F', 'Group G'
]);
});
it('cannot be deleted if it is in use', function () {
this.model.set('usage', [ {'label': 'label1', 'url': 'url1'} ]);
this.view.render();
expect(this.view.$(SELECTORS.note)).toHaveAttr(
'data-tooltip', 'Cannot delete when in use by an experiment'
);
expect(this.view.$('.delete')).toHaveClass('is-disabled');
});
});
describe('GroupConfigurationsList', function() {
var emptyMessage = 'You haven\'t created any group configurations yet.';
beforeEach(function() {
view_helpers.installTemplate('no-group-configurations', true);
this.model = new GroupConfigurationModel({ id: 0 });
this.collection = new GroupConfigurationCollection();
this.view = new GroupConfigurationsList({
collection: this.collection
@@ -415,39 +436,52 @@ define([
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).toContainText(emptyMessage);
expect(this.view.$('.new-button')).toExist();
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([{}]);
this.collection.add(this.model);
expect(this.view.$el).not.toContainText(emptyMessage);
expect(this.view.$(SELECTORS.itemView)).toExist();
});
it('should appear if configurations were removed', function() {
this.collection.add(this.model);
expect(this.view.$(SELECTORS.itemView)).toExist();
this.collection.remove(this.model);
expect(this.view.$el).toContainText(emptyMessage);
expect(this.view.$(SELECTORS.itemView)).not.toExist();
});
});
});
describe('GroupConfigurationItem', function() {
var clickDeleteItem;
beforeEach(function() {
view_helpers.installTemplates([
'group-configuration-edit', 'group-configuration-details'
], true);
this.model = new GroupConfigurationModel({ id: 0 });
this.collection = new GroupConfigurationCollection([ this.model ]);
this.collection.url = '/group_configurations';
this.view = new GroupConfigurationItem({
model: this.model
});
appendSetFixtures(this.view.render().el);
});
clickDeleteItem = function (view, promptSpy) {
view.$('.delete').click();
view_helpers.verifyPromptShowing(promptSpy, /Delete this Group Configuration/);
view_helpers.confirmPrompt(promptSpy);
view_helpers.verifyPromptHidden(promptSpy);
};
it('should render properly', function() {
// Details view by default
expect(this.view.$(SELECTORS.detailsView)).toExist();
@@ -458,6 +492,40 @@ define([
expect(this.view.$(SELECTORS.detailsView)).toExist();
expect(this.view.$(SELECTORS.editView)).not.toExist();
});
it('should destroy itself on confirmation of deleting', function () {
var requests = create_sinon.requests(this),
promptSpy = view_helpers.createPromptSpy(),
notificationSpy = view_helpers.createNotificationSpy();
clickDeleteItem(this.view, promptSpy);
// Backbone.emulateHTTP is enabled in our system, so setting this
// option will fake PUT, PATCH and DELETE requests with a HTTP POST,
// setting the X-HTTP-Method-Override header with the true method.
create_sinon.expectJsonRequest(requests, 'POST', '/group_configurations/0');
expect(_.last(requests).requestHeaders['X-HTTP-Method-Override']).toBe('DELETE');
view_helpers.verifyNotificationShowing(notificationSpy, /Deleting/);
create_sinon.respondToDelete(requests);
view_helpers.verifyNotificationHidden(notificationSpy);
expect($(SELECTORS.itemView)).not.toExist();
});
it('does not hide deleting message if failure', function() {
var requests = create_sinon.requests(this),
promptSpy = view_helpers.createPromptSpy(),
notificationSpy = view_helpers.createNotificationSpy();
clickDeleteItem(this.view, promptSpy);
// Backbone.emulateHTTP is enabled in our system, so setting this
// option will fake PUT, PATCH and DELETE requests with a HTTP POST,
// setting the X-HTTP-Method-Override header with the true method.
create_sinon.expectJsonRequest(requests, 'POST', '/group_configurations/0');
expect(_.last(requests).requestHeaders['X-HTTP-Method-Override']).toBe('DELETE');
view_helpers.verifyNotificationShowing(notificationSpy, /Deleting/);
create_sinon.respondWithError(requests);
view_helpers.verifyNotificationShowing(notificationSpy, /Deleting/);
expect($(SELECTORS.itemView)).toExist();
});
});
describe('GroupEdit', function() {

View File

@@ -1,5 +1,5 @@
define(["sinon", "underscore"], function(sinon, _) {
var fakeServer, fakeRequests, respondWithJson, respondWithError;
var fakeServer, fakeRequests, expectJsonRequest, respondWithJson, respondWithError, respondToDelete;
/* These utility methods are used by Jasmine tests to create a mock server or
* get reference to mock requests. In either case, the cleanup (restore) is done with
@@ -45,6 +45,17 @@ define(["sinon", "underscore"], function(sinon, _) {
return requests;
};
expectJsonRequest = function(requests, method, url, jsonRequest, requestIndex) {
var request;
if (_.isUndefined(requestIndex)) {
requestIndex = requests.length - 1;
}
request = requests[requestIndex];
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(JSON.parse(request.requestBody)).toEqual(jsonRequest);
};
respondWithJson = function(requests, jsonResponse, requestIndex) {
if (_.isUndefined(requestIndex)) {
requestIndex = requests.length - 1;
@@ -63,10 +74,20 @@ define(["sinon", "underscore"], function(sinon, _) {
JSON.stringify({ }));
};
respondToDelete = function(requests, requestIndex) {
if (_.isUndefined(requestIndex)) {
requestIndex = requests.length - 1;
}
requests[requestIndex].respond(204,
{ "Content-Type": "application/json" });
};
return {
"server": fakeServer,
"requests": fakeRequests,
"expectJsonRequest": expectJsonRequest,
"respondWithJson": respondWithJson,
"respondWithError": respondWithError
"respondWithError": respondWithError,
"respondToDelete": respondToDelete
};
});

View File

@@ -1,19 +1,22 @@
/**
* Provides helper methods for invoking Studio modal windows in Jasmine tests.
*/
define(["jquery", "js/views/feedback_notification", "js/spec_helpers/create_sinon"],
function($, NotificationView, create_sinon) {
var installTemplate, installTemplates, installViewTemplates, createNotificationSpy,
verifyNotificationShowing, verifyNotificationHidden;
define(['jquery', 'js/views/feedback_notification', 'js/views/feedback_prompt'],
function($, NotificationView, Prompt) {
'use strict';
var installTemplate, installTemplates, installViewTemplates, createFeedbackSpy, verifyFeedbackShowing,
verifyFeedbackHidden, createNotificationSpy, verifyNotificationShowing,
verifyNotificationHidden, createPromptSpy, confirmPrompt, verifyPromptShowing,
verifyPromptHidden;
installTemplate = function(templateName, isFirst) {
var template = readFixtures(templateName + '.underscore'),
templateId = templateName + '-tpl';
if (isFirst) {
setFixtures($("<script>", { id: templateId, type: "text/template" }).text(template));
setFixtures($('<script>', { id: templateId, type: 'text/template' }).text(template));
} else {
appendSetFixtures($("<script>", { id: templateId, type: "text/template" }).text(template));
appendSetFixtures($('<script>', { id: templateId, type: 'text/template' }).text(template));
}
};
@@ -35,22 +38,56 @@ define(["jquery", "js/views/feedback_notification", "js/spec_helpers/create_sino
appendSetFixtures('<div id="page-notification"></div>');
};
createNotificationSpy = function(type) {
var notificationSpy = spyOnConstructor(NotificationView, type || "Mini", ["show", "hide"]);
notificationSpy.show.andReturn(notificationSpy);
return notificationSpy;
createFeedbackSpy = function(type, intent) {
var feedbackSpy = spyOnConstructor(type, intent, ['show', 'hide']);
feedbackSpy.show.andReturn(feedbackSpy);
return feedbackSpy;
};
verifyNotificationShowing = function(notificationSpy, text) {
expect(notificationSpy.constructor).toHaveBeenCalled();
expect(notificationSpy.show).toHaveBeenCalled();
expect(notificationSpy.hide).not.toHaveBeenCalled();
var options = notificationSpy.constructor.mostRecentCall.args[0];
verifyFeedbackShowing = function(feedbackSpy, text) {
var options;
expect(feedbackSpy.constructor).toHaveBeenCalled();
expect(feedbackSpy.show).toHaveBeenCalled();
expect(feedbackSpy.hide).not.toHaveBeenCalled();
options = feedbackSpy.constructor.mostRecentCall.args[0];
expect(options.title).toMatch(text);
};
verifyFeedbackHidden = function(feedbackSpy) {
expect(feedbackSpy.hide).toHaveBeenCalled();
};
createNotificationSpy = function(type) {
return createFeedbackSpy(NotificationView, type || 'Mini');
};
verifyNotificationShowing = function(notificationSpy, text) {
verifyFeedbackShowing.apply(this, arguments);
};
verifyNotificationHidden = function(notificationSpy) {
expect(notificationSpy.hide).toHaveBeenCalled();
verifyFeedbackHidden.apply(this, arguments);
};
createPromptSpy = function(type) {
return createFeedbackSpy(Prompt, type || 'Warning');
};
confirmPrompt = function(promptSpy, pressSecondaryButton) {
expect(promptSpy.constructor).toHaveBeenCalled();
if (pressSecondaryButton) {
promptSpy.constructor.mostRecentCall.args[0].actions.secondary.click(promptSpy);
} else {
promptSpy.constructor.mostRecentCall.args[0].actions.primary.click(promptSpy);
}
};
verifyPromptShowing = function(promptSpy, text) {
verifyFeedbackShowing.apply(this, arguments);
};
verifyPromptHidden = function(promptSpy) {
verifyFeedbackHidden.apply(this, arguments);
};
return {
@@ -59,6 +96,10 @@ define(["jquery", "js/views/feedback_notification", "js/spec_helpers/create_sino
'installViewTemplates': installViewTemplates,
'createNotificationSpy': createNotificationSpy,
'verifyNotificationShowing': verifyNotificationShowing,
'verifyNotificationHidden': verifyNotificationHidden
'verifyNotificationHidden': verifyNotificationHidden,
'confirmPrompt': confirmPrompt,
'createPromptSpy': createPromptSpy,
'verifyPromptShowing': verifyPromptShowing,
'verifyPromptHidden': verifyPromptHidden
};
});

View File

@@ -42,11 +42,11 @@ function(BaseView, _, $, gettext, GroupEdit) {
uniqueId: _.uniqueId(),
name: this.model.escape('name'),
description: this.model.escape('description'),
usage: this.model.get('usage'),
isNew: this.model.isNew(),
error: this.model.validationError
}));
this.addAll();
return this;
},

View File

@@ -10,6 +10,9 @@ define([
attributes: {
'tabindex': -1
},
events: {
'click .delete': 'deleteConfiguration'
},
className: function () {
var index = this.model.collection.indexOf(this.model);
@@ -26,6 +29,24 @@ define([
this.listenTo(this.model, 'remove', this.remove);
},
deleteConfiguration: function(event) {
if(event && event.preventDefault) { event.preventDefault(); }
var self = this;
this.confirmThenRunOperation(
gettext('Delete this Group Configuration?'),
gettext('Deleting this Group Configuration is permanent and cannot be undone.'),
gettext('Delete'),
function() {
return self.runOperationShowingMessage(
gettext('Deleting') + '&hellip;',
function () {
return self.model.destroy({ wait: true });
}
);
}
);
},
render: function() {
// Removes a view from the DOM, and calls stopListening to remove
// any bound events that the view has listened to.

View File

@@ -14,6 +14,7 @@ define([
initialize: function() {
this.emptyTemplate = this.loadTemplate('no-group-configurations');
this.listenTo(this.collection, 'add', this.addNewItemView);
this.listenTo(this.collection, 'remove', this.handleDestory);
},
render: function() {
@@ -57,6 +58,12 @@ define([
addOne: function(event) {
if(event && event.preventDefault) { event.preventDefault(); }
this.collection.add([{ editing: true }]);
},
handleDestory: function () {
if(this.collection.length === 0) {
this.$el.html(this.emptyTemplate());
}
}
});

View File

@@ -172,12 +172,22 @@
.action {
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
.edit {
@include blue-button;
@extend %t-action4;
}
.delete {
@extend %ui-btn-non;
&.is-disabled {
background-color: $gray-l3;
color: $gray-l6;
}
}
}
}
}
@@ -199,255 +209,264 @@
}
}
&:hover .wrapper-group-configuration .actions {
opacity: 1.0;
}
}
.group-configuration-edit {
@include box-sizing(border-box);
border-radius: 2px;
width: 100%;
background: $white;
.group-configuration-edit {
@include box-sizing(border-box);
border-radius: 2px;
width: 100%;
background: $white;
.wrapper-form {
padding: $baseline ($baseline*1.5);
}
.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;
}
.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;
}
.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;
.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);
.action {
margin-right: ($baseline/4);
&:last-child {
margin-right: 0;
}
}
&: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;
}
.wrapper-delete-button {
float: right;
padding: ($baseline/4) ($baseline/2);
.is-disabled {
color: $gray-l3;
}
}
}
// 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;
}
.copy {
@extend %t-copy-sub2;
margin: ($baseline) 0 ($baseline/2) 0;
color: $gray;
.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;
}
}
.groups-fields,
.group-configuration-fields {
@extend %cont-no-list;
.field {
margin: 0 0 ($baseline*0.75) 0;
&:last-child {
margin-bottom: 0;
strong {
font-weight: 600;
}
}
&.required {
.groups-fields,
.group-configuration-fields {
@extend %cont-no-list;
label {
font-weight: 600;
.field {
margin: 0 0 ($baseline*0.75) 0;
&:last-child {
margin-bottom: 0;
}
label:after {
&.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;
display: inline-block;
width: 50%;
padding-right: 5%;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: bottom;
}
.group-configuration-id {
display: inline-block;
width: 45%;
text-align: right;
vertical-align: top;
color: $gray-l1;
.group-configuration-value {
font-weight: 600;
white-space: nowrap;
margin-left: ($baseline*0.5);
}
}
&.group-allocation {
color: $gray-l1;
}
}
label.required {
font-weight: 600;
&:after {
margin-left: ($baseline/4);
content: "*";
}
}
label, input, textarea {
display: block;
}
.field-group {
@include clearfix();
margin: 0 0 ($baseline/2) 0;
padding: ($baseline/4) 0 0 0;
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%;
.group-allocation,
.field {
display: inline-block;
vertical-align: middle;
margin: 0 3% 0 0;
}
&.short {
width: 25%;
.group-allocation {
max-width: 10%;
min-width: 5%;
color: $gray-l1;
}
::-webkit-input-placeholder {
color: $gray-l4;
}
.field {
position: relative;
:-moz-placeholder {
color: $gray-l3;
}
::-moz-placeholder {
color: $gray-l3;
}
:-ms-input-placeholder {
color: $gray-l3;
}
&:focus {
+ .tip {
color: $gray;
}
}
}
&.error {
label {
color: $red;
&.long {
width: 80%;
}
input {
border-color: $red;
&.short {
width: 10%;
}
}
&.add-group-configuration-name label {
@extend %t-title5;
display: inline-block;
width: 50%;
padding-right: 5%;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: bottom;
}
.group-configuration-id {
display: inline-block;
width: 45%;
text-align: right;
vertical-align: top;
color: $gray-l1;
.group-configuration-value {
font-weight: 600;
white-space: nowrap;
margin-left: ($baseline*0.5);
}
}
&.group-allocation {
color: $gray-l1;
.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;
}
}
}
}
label.required {
font-weight: 600;
&:after {
margin-left: ($baseline/4);
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;
}
}
.group-configuration-fields {
margin-bottom: $baseline;
&:hover .wrapper-group-configuration .actions {
opacity: 1.0;
}
.action-add-group {