FIX: Many bugs with admin badges interface

* Editing a badge's title would show it as changed in the side even if
  you didn't hit save

* Clicking a badge would not scroll to the top

* If there was an error saving a badge there was a missing i18n key

* URLs were using queryParams instead of paths

* User `label` tags for checkboxes for larger click targets

* Saved! text would persist when viewing another badge

* After creating a new badge it would show nothing

* Validation errors were not being properly released to the client

* Query errors were surrounded by an extra array
This commit is contained in:
Robin Ward 2014-10-17 14:27:40 -04:00
parent ab9a0235b4
commit 0cbdf6f5bb
17 changed files with 359 additions and 495 deletions

View file

@ -1,37 +0,0 @@
import ObjectController from 'discourse/controllers/object';
/**
This is the itemController for `Discourse.AdminBadgesController`. Its main purpose
is to indicate which badge was selected.
@class AdminBadgeController
@extends ObjectController
@namespace Discourse
@module Discourse
**/
export default ObjectController.extend({
/**
Whether this badge has been selected.
@property selected
@type {Boolean}
**/
selected: Discourse.computed.propertyEqual('model.name', 'parentController.selectedItem.name'),
/**
Show the displayName only if it is different from the name.
@property showDisplayName
@type {Boolean}
**/
showDisplayName: Discourse.computed.propertyNotEqual('selectedItem.name', 'selectedItem.displayName'),
/**
Don't allow editing if this is a system badge.
@property readOnly
@type {Boolean}
**/
readOnly: Ember.computed.alias('model.system')
});

View file

@ -0,0 +1,98 @@
import BufferedContent from 'discourse/mixins/buffered-content';
export default Ember.ObjectController.extend(BufferedContent, {
needs: ['admin-badges'],
saving: false,
savingStatus: '',
badgeTypes: Em.computed.alias('controllers.admin-badges.badgeTypes'),
badgeGroupings: Em.computed.alias('controllers.admin-badges.badgeGroupings'),
badgeTriggers: Em.computed.alias('controllers.admin-badges.badgeTriggers'),
protectedSystemFields: Em.computed.alias('controllers.admin-badges.protectedSystemFields'),
readOnly: Ember.computed.alias('buffered.system'),
showDisplayName: Discourse.computed.propertyNotEqual('name', 'displayName'),
canEditDescription: Em.computed.none('buffered.translatedDescription'),
_resetSaving: function() {
this.set('saving', false);
this.set('savingStatus', '');
}.observes('model.id'),
actions: {
save: function() {
if (!this.get('saving')) {
var fields = ['allow_title', 'multiple_grant',
'listable', 'auto_revoke',
'enabled', 'show_posts',
'target_posts', 'name', 'description',
'icon', 'query', 'badge_grouping_id',
'trigger', 'badge_type_id'],
self = this;
if (this.get('buffered.system')){
var protectedFields = this.get('protectedSystemFields');
fields = _.filter(fields, function(f){
return !_.include(protectedFields,f);
});
}
this.set('saving', true);
this.set('savingStatus', I18n.t('saving'));
var boolFields = ['allow_title', 'multiple_grant',
'listable', 'auto_revoke',
'enabled', 'show_posts',
'target_posts' ];
var data = {},
buffered = this.get('buffered');
fields.forEach(function(field){
var d = buffered.get(field);
if (_.include(boolFields, field)) { d = !!d; }
data[field] = d;
});
var newBadge = !this.get('id'),
model = this.get('model');
this.get('model').save(data).then(function() {
if (newBadge) {
self.get('controllers.admin-badges').pushObject(model);
self.transitionToRoute('adminBadges.show', model.get('id'));
} else {
self.commitBuffer();
self.set('savingStatus', I18n.t('saved'));
}
}).catch(function(error) {
self.set('savingStatus', I18n.t('failed'));
self.send('saveError', error);
}).finally(function() {
self.set('saving', false);
});
}
},
destroy: function() {
var self = this,
adminBadgesController = this.get('controllers.admin-badges'),
model = this.get('model');
if (!model.get('id')) {
self.transitionToRoute('adminBadges.index');
return;
}
return bootbox.confirm(I18n.t("admin.badges.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
if (result) {
model.destroy().then(function() {
adminBadgesController.removeObject(model);
self.transitionToRoute('adminBadges.index');
}).catch(function() {
bootbox.alert(I18n.t('generic_error'));
});
}
});
}
}
});

View file

@ -1,165 +1 @@
/**
This controller supports the interface for dealing with badges.
@class AdminBadgesController
@extends Ember.ArrayController
@namespace Discourse
@module Discourse
**/
export default Ember.ArrayController.extend({
needs: ['modal'],
itemController: 'admin-badge',
queryParams: ['badgeId'],
badgeId: Em.computed.alias('selectedId'),
/**
ID of the currently selected badge.
@property selectedId
@type {Integer}
**/
selectedId: null,
/**
Badge that is currently selected.
@property selectedItem
@type {Discourse.Badge}
**/
selectedItem: function() {
if (this.get('selectedId') === undefined || this.get('selectedId') === "undefined") {
// New Badge
return this.get('newBadge');
} else {
// Existing Badge
var selectedId = parseInt(this.get('selectedId'));
return this.get('model').filter(function(badge) {
return parseInt(badge.get('id')) === selectedId;
})[0];
}
}.property('selectedId', 'newBadge'),
/**
Unsaved badge, if one exists.
@property newBadge
@type {Discourse.Badge}
**/
newBadge: function() {
return this.get('model').filter(function(badge) {
return badge.get('id') === undefined;
})[0];
}.property('model.@each.id'),
/**
Whether a new unsaved badge exists.
@property newBadgeExists
@type {Discourse.Badge}
**/
newBadgeExists: Em.computed.notEmpty('newBadge'),
/**
We don't allow setting a description if a translation for the given badge
name exists.
@property canEditDescription
@type {Boolean}
**/
canEditDescription: Em.computed.none('selectedItem.translatedDescription'),
/**
Disable saving if the currently selected item is being saved.
@property disableSave
@type {Boolean}
**/
disableSave: Em.computed.alias('selectedItem.saving'),
actions: {
/**
Create a new badge and select it.
@method newBadge
**/
createNewBadge: function() {
var badge = Discourse.Badge.create({
name: I18n.t('admin.badges.new_badge')
});
this.pushObject(badge);
this.send('selectBadge', badge);
},
/**
Select a particular badge.
@method selectBadge
@param {Discourse.Badge} badge The badge to be selected
**/
selectBadge: function(badge) {
this.set('selectedId', badge.get('id'));
},
/**
Save the selected badge.
@method save
**/
save: function() {
if (!this.get('disableSave')) {
var fields = ['allow_title', 'multiple_grant',
'listable', 'auto_revoke',
'enabled', 'show_posts',
'target_posts', 'name', 'description',
'icon', 'query', 'badge_grouping_id',
'trigger', 'badge_type_id'],
self = this;
if (this.get('selectedItem.system')){
var protectedFields = this.get('protectedSystemFields');
fields = _.filter(fields, function(f){
return !_.include(protectedFields,f);
});
}
this.get('selectedItem').save(fields).catch(function(error) {
// this shows the admin-badge-preview modal with the error
// kinda weird, but it consolidates the display logic for badge errors
self.send('saveError', error);
});
}
},
/**
Confirm before destroying the selected badge.
@method destroy
**/
destroy: function() {
// Delete immediately if the selected badge is new.
if (!this.get('selectedItem.id')) {
this.get('model').removeObject(this.get('selectedItem'));
this.set('selectedId', null);
return;
}
var self = this;
return bootbox.confirm(I18n.t("admin.badges.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
if (result) {
var selected = self.get('selectedItem');
selected.destroy().then(function() {
// Success.
self.set('selectedId', null);
self.get('model').removeObject(selected);
}, function() {
// Failure.
bootbox.alert(I18n.t('generic_error'));
});
}
});
}
}
});
export default Ember.ArrayController.extend();

View file

@ -0,0 +1,52 @@
export default Ember.Route.extend({
serialize: function(m) {
return {badge_id: Em.get(m, 'id') || 'new'};
},
model: function(params) {
if (params.badge_id === "new") {
return Discourse.Badge.create({
name: I18n.t('admin.badges.new_badge')
});
}
return this.modelFor('adminBadges').findProperty('id', parseInt(params.badge_id));
},
actions: {
saveError: function(e) {
var msg = I18n.t("generic_error");
if (e.responseJSON && e.responseJSON.errors) {
msg = I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')});
}
bootbox.alert(msg);
},
editGroupings: function() {
var groupings = this.controllerFor('admin-badges').get('badgeGroupings');
Discourse.Route.showModal(this, 'admin_edit_badge_groupings', groupings);
},
preview: function(badge, explain) {
var self = this;
badge.set('preview_loading', true);
Discourse.ajax('/admin/badges/preview.json', {
method: 'post',
data: {
sql: badge.get('query'),
target_posts: !!badge.get('target_posts'),
trigger: badge.get('trigger'),
explain: explain
}
}).then(function(json) {
badge.set('preview_loading', false);
Discourse.Route.showModal(self, 'admin_badge_preview', json);
}).catch(function(error) {
badge.set('preview_loading', false);
Em.Logger.error(error);
bootbox.alert("Network error");
});
}
}
});

View file

@ -0,0 +1,28 @@
export default Discourse.Route.extend({
_json: null,
model: function() {
var self = this;
return Discourse.ajax('/admin/badges.json').then(function(json) {
self._json = json;
return Discourse.Badge.createFromJson(json);
});
},
setupController: function(controller, model) {
var json = this._json,
triggers = [];
_.each(json.admin_badges.triggers,function(v,k){
triggers.push({id: v, name: I18n.t('admin.badges.trigger_type.'+k)});
});
controller.setProperties({
badgeGroupings: json.badge_groupings,
badgeTypes: json.badge_types,
protectedSystemFields: json.admin_badges.protected_system_fields,
badgeTriggers: triggers,
model: model
});
}
});

View file

@ -1,54 +0,0 @@
Discourse.AdminBadgesRoute = Discourse.Route.extend({
setupController: function(controller) {
Discourse.ajax('/admin/badges.json').then(function(json){
controller.set('badgeGroupings', Em.A(json.badge_groupings));
controller.set('badgeTypes', json.badge_types);
controller.set('protectedSystemFields', json.admin_badges.protected_system_fields);
var triggers = [];
_.each(json.admin_badges.triggers,function(v,k){
triggers.push({id: v, name: I18n.t('admin.badges.trigger_type.'+k)});
});
controller.set('badgeTriggers', triggers);
controller.set('model', Discourse.Badge.createFromJson(json));
});
},
actions: {
editGroupings: function(model) {
Discourse.Route.showModal(this, 'admin_edit_badge_groupings', model);
},
saveError: function(jqXhr) {
if (jqXhr.status === 422) {
Discourse.Route.showModal(this, 'admin_badge_preview', jqXhr.responseJSON);
} else {
Em.Logger.error(jqXhr);
bootbox.alert(I18n.t('errors.description.unknown'));
}
},
preview: function(badge, explain) {
var self = this;
badge.set('preview_loading', true);
Discourse.ajax('/admin/badges/preview.json', {
method: 'post',
data: {
sql: badge.query,
target_posts: !!badge.target_posts,
trigger: badge.trigger,
explain: explain
}
}).then(function(json) {
badge.set('preview_loading', false);
Discourse.Route.showModal(self, 'admin_badge_preview', json);
}).catch(function(error) {
badge.set('preview_loading', false);
Em.Logger.error(error);
bootbox.alert("Network error");
});
}
}
});

View file

@ -58,7 +58,9 @@ Discourse.Route.buildRoutes(function() {
});
});
this.route('badges');
this.resource('adminBadges', { path: '/badges' }, function() {
this.route('show', { path: '/:badge_id' });
});
});
});

View file

@ -10,7 +10,7 @@
{{/if}}
<li>{{#link-to 'adminUsersList'}}{{i18n admin.users.title}}{{/link-to}}</li>
{{#if showBadges}}
<li>{{#link-to 'admin.badges'}}{{i18n admin.badges.title}}{{/link-to}}</li>
<li>{{#link-to 'adminBadges.index'}}{{i18n admin.badges.title}}{{/link-to}}</li>
{{/if}}
{{#if currentUser.admin}}
<li>{{#link-to 'adminGroups.index'}}{{i18n admin.groups.title}}{{/link-to}}</li>

View file

@ -0,0 +1,9 @@
<div class='span13'>
<p>{{i18n admin.badges.none_selected}}</p>
<div>
{{#link-to 'adminBadges.show' 'new' class="btn"}}
{{fa-icon "plus"}} {{i18n admin.badges.new}}
{{/link-to}}
</div>
</div>

View file

@ -0,0 +1,140 @@
<div class='current-badge span13'>
<form class="form-horizontal">
<div>
<label for="name">{{i18n admin.badges.name}}</label>
{{input type="text" name="name" value=buffered.name}}
</div>
{{#if showDisplayName}}
<div>
<strong>{{i18n admin.badges.display_name}}</strong>
{{buffered.displayName}}
</div>
{{/if}}
<div>
<label for="name">{{i18n admin.badges.icon}}</label>
{{input type="text" name="name" value=buffered.icon}}
<p class='help'>{{i18n admin.badges.icon_help}}</p>
</div>
<div>
<label for="badge_type_id">{{i18n admin.badges.badge_type}}</label>
{{view Ember.Select name="badge_type_id"
value=buffered.badge_type_id
content=badgeTypes
optionValuePath="content.id"
optionLabelPath="content.name"
disabled=readOnly}}
</div>
<div>
<label for="badge_grouping_id">{{i18n admin.badges.badge_grouping}}</label>
{{view Ember.Select name="badge_grouping_id"
value=buffered.badge_grouping_id
content=badgeGroupings
optionValuePath="content.id"
optionLabelPath="content.name"}}
&nbsp;<button {{action "editGroupings"}} class='btn'>{{fa-icon 'pencil'}}</button>
</div>
<div>
<label for="description">{{i18n admin.badges.description}}</label>
{{#if canEditDescription}}
{{textarea name="description" value=buffered.description}}
{{else}}
{{textarea name="description" value=buffered.displayDescription disabled=true}}
{{/if}}
</div>
<div>
<label for="query">{{i18n admin.badges.query}}</label>
{{textarea name="query" value=buffered.query disabled=readOnly}}
</div>
{{#if hasQuery}}
<a href {{action "preview" buffered "false"}}>{{i18n admin.badges.preview.link_text}}</a>
|
<a href {{action "preview" buffered "true"}}>{{i18n admin.badges.preview.plan_text}}</a>
{{#if preview_loading}}
{{i18n loading}}...
{{/if}}
<div>
<label>
{{input type="checkbox" checked=buffered.auto_revoke disabled=readOnly}}
{{i18n admin.badges.auto_revoke}}
</label>
</div>
<div>
<label>
{{input type="checkbox" checked=buffered.target_posts disabled=readOnly}}
{{i18n admin.badges.target_posts}}
</label>
</div>
<div>
<label for="trigger">{{i18n admin.badges.trigger}}</label>
{{view Ember.Select name="trigger"
value=buffered.trigger
content=badgeTriggers
optionValuePath="content.id"
optionLabelPath="content.name"
disabled=readOnly}}
</div>
{{/if}}
<div>
<label>
{{input type="checkbox" checked=buffered.allow_title}}
{{i18n admin.badges.allow_title}}
</label>
</div>
<div>
<label>
{{input type="checkbox" checked=buffered.multiple_grant disabled=readOnly}}
{{i18n admin.badges.multiple_grant}}
</label>
</div>
<div>
<label>
{{input type="checkbox" checked=buffered.listable disabled=readOnly}}
{{i18n admin.badges.listable}}
</label>
</div>
<div>
<label>
{{input type="checkbox" checked=buffered.show_posts disabled=readOnly}}
{{i18n admin.badges.show_posts}}
</label>
</div>
<div>
<label>
{{input type="checkbox" checked=buffered.enabled}}
{{i18n admin.badges.enabled}}
</label>
</div>
<div class='buttons'>
<button {{action "save"}} {{bind-attr disabled=saving}} class='btn btn-primary'>{{i18n admin.badges.save}}</button>
<span class='saving'>{{savingStatus}}</span>
{{#unless readOnly}}
<a {{action "destroy"}} class='delete-link'>{{i18n admin.badges.delete}}</a>
{{/unless}}
</div>
</form>
</div>
{{#if grant_count}}
<div class="span13 current-badge-actions">
<div>
{{#link-to 'badges.show' this}}{{i18n badges.granted count=grant_count}}{{/link-to}}
</div>
</div>
{{/if}}

View file

@ -5,160 +5,21 @@
<ul>
{{#each}}
<li>
<a {{action "selectBadge" this}} {{bind-attr class="selected:active"}}>
{{#link-to 'adminBadges.show' id}}
{{badge-button badge=this}}
{{#if newBadge}}
<span class="list-badge">{{i18n filters.new.lower_title}}</span>
{{/if}}
</a>
{{/link-to}}
</li>
{{/each}}
</ul>
<button {{action "createNewBadge"}} {{bind-attr disabled=newBadgeExists}} class='btn'><i class="fa fa-plus"></i>{{i18n admin.badges.new}}</button>
{{#link-to 'adminBadges.show' 'new' class="btn"}}
{{fa-icon "plus"}} {{i18n admin.badges.new}}
{{/link-to}}
<br>
<br>
</div>
{{#if selectedItem}}
{{#with selectedItem controller='adminBadge'}}
<div class='current-badge span13'>
<form class="form-horizontal">
<div>
<label for="name">{{i18n admin.badges.name}}</label>
{{input type="text" name="name" value=name}}
</div>
{{#if showDisplayName}}
<div>
<strong>{{i18n admin.badges.display_name}}</strong>
{{displayName}}
</div>
{{/if}}
<div>
<label for="name">{{i18n admin.badges.icon}}</label>
{{input type="text" name="name" value=icon}}
<p class='help'>{{i18n admin.badges.icon_help}}</p>
</div>
<div>
<label for="badge_type_id">{{i18n admin.badges.badge_type}}</label>
{{view Ember.Select name="badge_type_id" value=badge_type_id
content=controller.badgeTypes
optionValuePath="content.id"
optionLabelPath="content.name"
disabled=readOnly}}
</div>
<div>
<label for="badge_grouping_id">{{i18n admin.badges.badge_grouping}}</label>
{{view Ember.Select name="badge_grouping_id" value=badge_grouping_id
content=controller.badgeGroupings
optionValuePath="content.id"
optionLabelPath="content.name"}}
&nbsp;<button {{action "editGroupings" controller.badgeGroupings}}><i class="fa fa-pencil"></i></button>
</div>
<div>
<label for="description">{{i18n admin.badges.description}}</label>
{{#if controller.canEditDescription}}
{{textarea name="description" value=description}}
{{else}}
{{textarea name="description" value=displayDescription disabled=true}}
{{/if}}
</div>
<div>
<label for="query">{{i18n admin.badges.query}}</label>
{{textarea name="query" value=query disabled=readOnly}}
</div>
{{#if hasQuery}}
<a href {{action "preview" this "false"}}>{{i18n admin.badges.preview.link_text}}</a>
|
<a href {{action "preview" this "true"}}>{{i18n admin.badges.preview.plan_text}}</a>
{{#if preview_loading}}
{{i18n loading}}...
{{/if}}
<div>
<span>
{{input type="checkbox" checked=auto_revoke disabled=readOnly}}
{{i18n admin.badges.auto_revoke}}
</span>
</div>
<div>
<span>
{{input type="checkbox" checked=target_posts disabled=readOnly}}
{{i18n admin.badges.target_posts}}
</span>
</div>
<div>
<label for="trigger">{{i18n admin.badges.trigger}}</label>
{{view Ember.Select name="trigger" value=trigger
content=controller.badgeTriggers
optionValuePath="content.id"
optionLabelPath="content.name"
disabled=readOnly}}
</div>
{{/if}}
<div>
<span>
{{input type="checkbox" checked=allow_title}}
{{i18n admin.badges.allow_title}}
</span>
</div>
<div>
<span>
{{input type="checkbox" checked=multiple_grant disabled=readOnly}}
{{i18n admin.badges.multiple_grant}}
</span>
</div>
<div>
<span>
{{input type="checkbox" checked=listable disabled=readOnly}}
{{i18n admin.badges.listable}}
</span>
</div>
<div>
<span>
{{input type="checkbox" checked=show_posts disabled=readOnly}}
{{i18n admin.badges.show_posts}}
</span>
</div>
<div>
<span>
{{input type="checkbox" checked=enabled}}
{{i18n admin.badges.enabled}}
</span>
</div>
<div class='buttons'>
<button {{action "save"}} {{bind-attr disabled=controller.disableSave}} class='btn btn-primary'>{{i18n admin.badges.save}}</button>
<span class='saving'>{{savingStatus}}</span>
{{#unless readOnly}}
<a {{action "destroy"}} class='delete-link'>{{i18n admin.badges.delete}}</a>
{{/unless}}
</div>
</form>
</div>
{{#if grant_count}}
<div class="span13 current-badge-actions">
<div>
{{#link-to 'badges.show' this}}{{i18n badges.granted count=grant_count}}{{/link-to}}
</div>
</div>
{{/if}}
{{/with}}
{{/if}}
{{outlet}}
</div>

View file

@ -0,0 +1 @@
export default Ember.View.extend(Discourse.ScrollTop);

View file

@ -0,0 +1,5 @@
export default Ember.View.extend(Discourse.ScrollTop, {
_scrollOnModelChange: function() {
this._scrollTop();
}.observes('controller.model.id')
});

View file

@ -110,45 +110,24 @@ Discourse.Badge = Discourse.Model.extend({
@method save
@returns {Promise} A promise that resolves to the updated `Discourse.Badge`
**/
save: function(fields) {
this.set('savingStatus', I18n.t('saving'));
this.set('saving', true);
save: function(data) {
var url = "/admin/badges",
requestType = "POST",
self = this;
if (!this.get('newBadge')) {
if (this.get('id')) {
// We are updating an existing badge.
url += "/" + this.get('id');
requestType = "PUT";
}
var boolFields = ['allow_title', 'multiple_grant',
'listable', 'auto_revoke',
'enabled', 'show_posts',
'target_posts' ];
var data = {};
fields.forEach(function(field){
var d = self.get(field);
if(_.include(boolFields, field)) {
d = !!d;
}
data[field] = d;
});
return Discourse.ajax(url, {
type: requestType,
data: data
}).then(function(json) {
self.updateFromJson(json);
self.set('savingStatus', I18n.t('saved'));
self.set('saving', false);
return self;
}).catch(function(error) {
self.set('savingStatus', I18n.t('failed'));
self.set('saving', false);
throw error;
});
},

View file

@ -20,6 +20,12 @@ class Admin::BadgesController < Admin::AdminController
trigger: params[:trigger].to_i)
end
def new
end
def show
end
def badge_types
badge_types = BadgeType.all.to_a
render_serialized(badge_types, BadgeTypeSerializer, root: "badge_types")
@ -98,7 +104,7 @@ class Admin::BadgesController < Admin::AdminController
begin
BadgeGranter.contract_checks!(badge.query, { target_posts: badge.target_posts, trigger: badge.trigger })
rescue => e
errors << [e.message]
errors << e.message
raise ActiveRecord::Rollback
end
@ -106,10 +112,9 @@ class Admin::BadgesController < Admin::AdminController
badge.save!
end
if badge.errors
errors
rescue ActiveRecord::RecordInvalid
errors.push(*badge.errors.full_messages)
end
errors
end
end

View file

@ -2095,6 +2095,7 @@ en:
grant: Grant
no_user_badges: "%{name} has not been granted any badges."
no_badges: There are no badges that can be granted.
none_selected: "Select a badge to get started"
allow_title: Allow badge to be used as a title
multiple_grant: Can be granted multiple times
listable: Show badge on the public badges page

View file

@ -1,62 +0,0 @@
moduleFor("controller:admin-badges", "controller:admin-badges", {
needs: ['controller:modal', 'controller:admin-badge']
});
test("canEditDescription", function() {
var badge = Discourse.Badge.create({id: 101, name: "Test Badge"});
var controller = this.subject({ model: [badge] });
controller.send('selectBadge', badge);
ok(controller.get('canEditDescription'), "allows editing description when a translation exists for the badge name");
badge.set('translatedDescription', 'translated');
ok(!controller.get('canEditDescription'), "can't edit the description when it's got a translation");
});
test("createNewBadge", function() {
var controller = this.subject();
controller.send('createNewBadge');
equal(controller.get('model.length'), 1, "adds a new badge to the list of badges");
});
test("selectBadge", function() {
var badge = Discourse.Badge.create({id: 101, name: "Test Badge"}),
controller = this.subject({ model: [badge] });
controller.send('selectBadge', badge);
equal(controller.get('selectedItem'), badge, "the badge is selected");
});
test("save", function() {
var badge = Discourse.Badge.create({id: 101, name: "Test Badge"}),
otherBadge = Discourse.Badge.create({id: 102, name: "Other Badge"}),
controller = this.subject({ model: [badge, otherBadge] });
controller.send('selectBadge', badge);
sandbox.stub(badge, "save").returns(Ember.RSVP.resolve({}));
controller.send("save");
ok(badge.save.calledOnce, "called save on the badge");
});
test("destroy", function() {
var badge = Discourse.Badge.create({id: 101, name: "Test Badge"}),
otherBadge = Discourse.Badge.create({id: 102, name: "Other Badge"}),
controller = this.subject({model: [badge, otherBadge]});
sandbox.stub(badge, 'destroy').returns(Ember.RSVP.resolve({}));
bootbox.confirm = function(text, yes, no, func) {
func(false);
};
controller.send('selectBadge', badge);
controller.send('destroy');
ok(!badge.destroy.calledOnce, "badge is not destroyed if they user clicks no");
bootbox.confirm = function(text, yes, no, func) {
func(true);
};
controller.send('selectBadge', badge);
controller.send('destroy');
ok(badge.destroy.calledOnce, "badge is destroyed if they user clicks yes");
});