Upon saving a badge or requesting a badge result preview, BadgeGranter.contract_checks! will examine the provided badge SQL for some contractual obligations - namely, the returned columns and use of trigger parameters. Saving the badge is wrapped in a transaction to make this easier, by raising ActiveRecord::Rollback on a detected violation. On the client, a modal view is added for the badge query sample run results, named admin-badge-preview. The preview action is moved up to the route. The save action, on failure, triggers a 'saveError' action (also in the route). The preview action gains a new parameter, 'explain', which will give the output of an EXPLAIN query for the badge sql, which can be used by forum admins to estimate the cost of their badge queries. The preview link is replaced by two links, one which omits (false) and includes (true) the EXPLAIN query. The Badge.save() method is amended to propogate errors. Badge::Trigger gets some utility methods for use in the BadgeGranter.contract_checks! method. Additionally, extra checks outside of BadgeGranter.contract_checks! are added in the preview() method, to cover cases of null granted_at columns. An uninitialized variable path is removed in the backfill() method. TODO - it would be nice to be able to get the actual names of all columns the provided query returns, so we could give more errors
244 lines
6.5 KiB
JavaScript
244 lines
6.5 KiB
JavaScript
/**
|
|
A data model representing a badge on Discourse
|
|
|
|
@class Badge
|
|
@extends Discourse.Model
|
|
@namespace Discourse
|
|
@module Discourse
|
|
**/
|
|
Discourse.Badge = Discourse.Model.extend({
|
|
/**
|
|
Is this a new badge?
|
|
|
|
@property newBadge
|
|
@type {String}
|
|
**/
|
|
newBadge: Em.computed.none('id'),
|
|
|
|
hasQuery: function(){
|
|
var query = this.get('query');
|
|
return query && query.trim().length > 0;
|
|
}.property('query'),
|
|
|
|
/**
|
|
@private
|
|
|
|
The name key to use for fetching i18n translations.
|
|
|
|
@property i18nNameKey
|
|
@type {String}
|
|
**/
|
|
i18nNameKey: function() {
|
|
return this.get('name').toLowerCase().replace(/\s/g, '_');
|
|
}.property('name'),
|
|
|
|
/**
|
|
The display name of this badge. Attempts to use a translation and falls back to
|
|
the actual name.
|
|
|
|
@property displayName
|
|
@type {String}
|
|
**/
|
|
displayName: function() {
|
|
var i18nKey = "badges.badge." + this.get('i18nNameKey') + ".name";
|
|
return I18n.t(i18nKey, {defaultValue: this.get('name')});
|
|
}.property('name', 'i18nNameKey'),
|
|
|
|
/**
|
|
The i18n translated description for this badge. Returns the null if no
|
|
translation exists.
|
|
|
|
@property translatedDescription
|
|
@type {String}
|
|
**/
|
|
translatedDescription: function() {
|
|
var i18nKey = "badges.badge." + this.get('i18nNameKey') + ".description",
|
|
translation = I18n.t(i18nKey);
|
|
if (translation.indexOf(i18nKey) !== -1) {
|
|
translation = null;
|
|
}
|
|
return translation;
|
|
}.property('i18nNameKey'),
|
|
|
|
displayDescription: function(){
|
|
// we support html in description but in most places do not need it
|
|
return this.get('displayDescriptionHtml').replace(/<[^>]*>/g, "");
|
|
}.property('displayDescriptionHtml'),
|
|
|
|
/**
|
|
Display-friendly description string. Returns either a translation or the
|
|
original description string.
|
|
|
|
@property displayDescription
|
|
@type {String}
|
|
**/
|
|
displayDescriptionHtml: function() {
|
|
var translated = this.get('translatedDescription');
|
|
return (translated === null ? this.get('description') : translated) || "";
|
|
}.property('description', 'translatedDescription'),
|
|
|
|
/**
|
|
Update this badge with the response returned by the server on save.
|
|
|
|
@method updateFromJson
|
|
@param {Object} json The JSON response returned by the server
|
|
**/
|
|
updateFromJson: function(json) {
|
|
var self = this;
|
|
if (json.badge) {
|
|
Object.keys(json.badge).forEach(function(key) {
|
|
self.set(key, json.badge[key]);
|
|
});
|
|
}
|
|
if (json.badge_types) {
|
|
json.badge_types.forEach(function(badgeType) {
|
|
if (badgeType.id === self.get('badge_type_id')) {
|
|
self.set('badge_type', Object.create(badgeType));
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
badgeTypeClassName: function() {
|
|
var type = this.get('badge_type.name') || "";
|
|
return "badge-type-" + type.toLowerCase();
|
|
}.property('badge_type.name'),
|
|
|
|
/**
|
|
Save and update the badge from the server's response.
|
|
|
|
@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);
|
|
|
|
var url = "/admin/badges",
|
|
requestType = "POST",
|
|
self = this;
|
|
|
|
if (!this.get('newBadge')) {
|
|
// 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;
|
|
});
|
|
},
|
|
|
|
/**
|
|
Destroy the badge.
|
|
|
|
@method destroy
|
|
@returns {Promise} A promise that resolves to the server response
|
|
**/
|
|
destroy: function() {
|
|
if (this.get('newBadge')) return Ember.RSVP.resolve();
|
|
return Discourse.ajax("/admin/badges/" + this.get('id'), {
|
|
type: "DELETE"
|
|
});
|
|
}
|
|
});
|
|
|
|
Discourse.Badge.reopenClass({
|
|
/**
|
|
Create `Discourse.Badge` instances from the server JSON response.
|
|
|
|
@method createFromJson
|
|
@param {Object} json The JSON returned by the server
|
|
@returns Array or instance of `Discourse.Badge` depending on the input JSON
|
|
**/
|
|
createFromJson: function(json) {
|
|
// Create BadgeType objects.
|
|
var badgeTypes = {};
|
|
if ('badge_types' in json) {
|
|
json.badge_types.forEach(function(badgeTypeJson) {
|
|
badgeTypes[badgeTypeJson.id] = Ember.Object.create(badgeTypeJson);
|
|
});
|
|
}
|
|
|
|
var badgeGroupings = {};
|
|
if ('badge_groupings' in json) {
|
|
json.badge_groupings.forEach(function(badgeGroupingJson) {
|
|
badgeGroupings[badgeGroupingJson.id] = Discourse.BadgeGrouping.create(badgeGroupingJson);
|
|
});
|
|
}
|
|
|
|
// Create Badge objects.
|
|
var badges = [];
|
|
if ("badge" in json) {
|
|
badges = [json.badge];
|
|
} else {
|
|
badges = json.badges;
|
|
}
|
|
badges = badges.map(function(badgeJson) {
|
|
var badge = Discourse.Badge.create(badgeJson);
|
|
badge.set('badge_type', badgeTypes[badge.get('badge_type_id')]);
|
|
badge.set('badge_grouping', badgeGroupings[badge.get('badge_grouping_id')]);
|
|
return badge;
|
|
});
|
|
|
|
if ("badge" in json) {
|
|
return badges[0];
|
|
} else {
|
|
return badges;
|
|
}
|
|
},
|
|
|
|
/**
|
|
Find all `Discourse.Badge` instances that have been defined.
|
|
|
|
@method findAll
|
|
@returns {Promise} a promise that resolves to an array of `Discourse.Badge`
|
|
**/
|
|
findAll: function(opts) {
|
|
var listable = "";
|
|
if(opts && opts.onlyListable){
|
|
listable = "?only_listable=true";
|
|
}
|
|
return Discourse.ajax('/badges.json' + listable).then(function(badgesJson) {
|
|
return Discourse.Badge.createFromJson(badgesJson);
|
|
});
|
|
},
|
|
|
|
/**
|
|
Returns a `Discourse.Badge` that has the given ID.
|
|
|
|
@method findById
|
|
@param {Number} id ID of the badge
|
|
@returns {Promise} a promise that resolves to a `Discourse.Badge`
|
|
**/
|
|
findById: function(id) {
|
|
return Discourse.ajax("/badges/" + id).then(function(badgeJson) {
|
|
return Discourse.Badge.createFromJson(badgeJson);
|
|
});
|
|
}
|
|
});
|