diff --git a/app/assets/javascripts/admin/controllers/admin_badge_controller.js b/app/assets/javascripts/admin/controllers/admin_badge_controller.js
index 3fb21eab684..084e5a8aab3 100644
--- a/app/assets/javascripts/admin/controllers/admin_badge_controller.js
+++ b/app/assets/javascripts/admin/controllers/admin_badge_controller.js
@@ -8,8 +8,6 @@
@module Discourse
**/
-var RESERVED_BADGE_COUNT = 100;
-
Discourse.AdminBadgeController = Discourse.ObjectController.extend({
/**
Whether this badge has been selected.
@@ -33,5 +31,5 @@ Discourse.AdminBadgeController = Discourse.ObjectController.extend({
@property readOnly
@type {Boolean}
**/
- readOnly: Ember.computed.lt('model.id', RESERVED_BADGE_COUNT)
+ readOnly: Ember.computed.alias('model.system')
});
diff --git a/app/assets/javascripts/admin/controllers/admin_badges_controller.js b/app/assets/javascripts/admin/controllers/admin_badges_controller.js
index baac6bf7fb7..6edc4c712a5 100644
--- a/app/assets/javascripts/admin/controllers/admin_badges_controller.js
+++ b/app/assets/javascripts/admin/controllers/admin_badges_controller.js
@@ -77,6 +77,21 @@ Discourse.AdminBadgesController = Ember.ArrayController.extend({
actions: {
+ preview: function(badge) {
+ // TODO wire modal and localize
+ Discourse.ajax('/admin/badges/preview.json', {
+ method: 'post',
+ data: {sql: badge.query, target_posts: !!badge.target_posts}
+ }).then(function(json){
+ if(json.error){
+ bootbox.alert(json.error);
+ } else {
+ bootbox.alert(json.grant_count + " badges to be assigned");
+ }
+ });
+
+ },
+
/**
Create a new badge and select it.
@@ -107,7 +122,21 @@ Discourse.AdminBadgesController = Ember.ArrayController.extend({
**/
save: function() {
if (!this.get('disableSave')) {
- this.get('selectedItem').save();
+ var fields = ['allow_title', 'multiple_grant',
+ 'listable', 'auto_revoke',
+ 'enabled', 'show_posts',
+ 'target_posts', 'name', 'description',
+ 'icon', 'query', 'badge_grouping_id',
+ 'trigger'];
+
+ if(this.get('selectedItem.system')){
+ var protectedFields = this.get('protectedSystemFields');
+ fields = _.filter(fields, function(f){
+ return !_.include(protectedFields,f);
+ });
+ }
+
+ this.get('selectedItem').save(fields);
}
},
diff --git a/app/assets/javascripts/admin/routes/admin_badges_route.js b/app/assets/javascripts/admin/routes/admin_badges_route.js
index 30da3e52de9..6ac596b5c3f 100644
--- a/app/assets/javascripts/admin/routes/admin_badges_route.js
+++ b/app/assets/javascripts/admin/routes/admin_badges_route.js
@@ -1,18 +1,16 @@
Discourse.AdminBadgesRoute = Discourse.Route.extend({
-
- model: function() {
- return Discourse.Badge.findAll();
- },
-
- setupController: function(controller, model) {
- // TODO build into findAll
- Discourse.ajax('/admin/badges/groupings').then(function(json) {
+ setupController: function(controller) {
+ Discourse.ajax('/admin/badges.json').then(function(json){
controller.set('badgeGroupings', json.badge_groupings);
- });
- Discourse.ajax('/admin/badges/types').then(function(json) {
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));
});
- controller.set('model', model);
}
});
diff --git a/app/assets/javascripts/admin/templates/badges.js.handlebars b/app/assets/javascripts/admin/templates/badges.js.handlebars
index 41759554d65..e5cf7e2f511 100644
--- a/app/assets/javascripts/admin/templates/badges.js.handlebars
+++ b/app/assets/javascripts/admin/templates/badges.js.handlebars
@@ -52,10 +52,10 @@
{{view Ember.Select name="badge_grouping_id" value=badge_grouping_id
content=controller.badgeGroupings
optionValuePath="content.id"
- optionLabelPath="content.name"
- disabled=readOnly}}
+ optionLabelPath="content.name"}}
+
{{#if controller.canEditDescription}}
@@ -65,6 +65,40 @@
{{/if}}
+
+
+ {{textarea name="query" value=query disabled=readOnly}}
+
+
+ {{#if hasQuery}}
+
+ {{i18n admin.badges.preview}}
+
+
+
+ {{input type="checkbox" checked=auto_revoke disabled=readOnly}}
+ {{i18n admin.badges.auto_revoke}}
+
+
+
+
+
+ {{input type="checkbox" checked=target_posts disabled=readOnly}}
+ {{i18n admin.badges.target_posts}}
+
+
+
+
+
+ {{view Ember.Select name="trigger" value=trigger
+ content=controller.badgeTriggers
+ optionValuePath="content.id"
+ optionLabelPath="content.name"
+ disabled=readOnly}}
+
+
+ {{/if}}
+
{{input type="checkbox" checked=allow_title disabled=readOnly}}
@@ -86,6 +120,13 @@
+
+
+ {{input type="checkbox" checked=show_posts disabled=readOnly}}
+ {{i18n admin.badges.show_posts}}
+
+
+
{{input type="checkbox" checked=enabled}}
diff --git a/app/assets/javascripts/discourse/models/badge.js b/app/assets/javascripts/discourse/models/badge.js
index fa0a13ff729..64fd1ec8870 100644
--- a/app/assets/javascripts/discourse/models/badge.js
+++ b/app/assets/javascripts/discourse/models/badge.js
@@ -15,6 +15,11 @@ Discourse.Badge = Discourse.Model.extend({
**/
newBadge: Em.computed.none('id'),
+ hasQuery: function(){
+ var query = this.get('query');
+ return query && query.trim().length > 0;
+ }.property('query'),
+
/**
@private
@@ -100,7 +105,7 @@ Discourse.Badge = Discourse.Model.extend({
@method save
@returns {Promise} A promise that resolves to the updated `Discourse.Badge`
**/
- save: function() {
+ save: function(fields) {
this.set('savingStatus', I18n.t('saving'));
this.set('saving', true);
@@ -114,19 +119,23 @@ Discourse.Badge = Discourse.Model.extend({
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: {
- name: this.get('name'),
- description: this.get('description'),
- badge_type_id: this.get('badge_type_id'),
- allow_title: !!this.get('allow_title'),
- multiple_grant: !!this.get('multiple_grant'),
- listable: !!this.get('listable'),
- enabled: !!this.get('enabled'),
- icon: this.get('icon'),
- badge_grouping_id: this.get('badge_grouping_id')
- }
+ data: data
}).then(function(json) {
self.updateFromJson(json);
self.set('savingStatus', I18n.t('saved'));
diff --git a/app/controllers/admin/badges_controller.rb b/app/controllers/admin/badges_controller.rb
index d75bd66d07e..186f4e44b3f 100644
--- a/app/controllers/admin/badges_controller.rb
+++ b/app/controllers/admin/badges_controller.rb
@@ -1,4 +1,20 @@
class Admin::BadgesController < Admin::AdminController
+
+ def index
+ data = {
+ badge_types: BadgeType.all.to_a,
+ badge_groupings: BadgeGrouping.all.to_a,
+ badges: Badge.all.to_a,
+ protected_system_fields: Badge.protected_system_fields,
+ triggers: Badge.trigger_hash
+ }
+ render_serialized(OpenStruct.new(data), AdminBadgesSerializer)
+ end
+
+ def preview
+ render json: BadgeGranter.preview(params[:sql], target_posts: params[:target_posts] == "true")
+ end
+
def badge_types
badge_types = BadgeType.all.to_a
render_serialized(badge_types, BadgeTypeSerializer, root: "badge_types")
@@ -35,7 +51,9 @@ class Admin::BadgesController < Admin::AdminController
end
def update_badge_from_params(badge)
- allowed = [:icon, :name, :description, :badge_type_id, :allow_title, :multiple_grant, :listable, :enabled, :badge_grouping_id]
+ allowed = Badge.column_names.map(&:to_sym)
+ allowed -= [:id, :created_at, :updated_at, :grant_count]
+ allowed -= Badge.protected_system_fields if badge.system?
params.permit(*allowed)
allowed.each do |key|
diff --git a/app/models/badge.rb b/app/models/badge.rb
index 51333529afb..35d3796ad8e 100644
--- a/app/models/badge.rb
+++ b/app/models/badge.rb
@@ -17,7 +17,16 @@ class Badge < ActiveRecord::Base
# other consts
AutobiographerMinBioLength = 10
+ def self.trigger_hash
+ Hash[*(
+ Badge::Trigger.constants.map{|k|
+ [k.to_s.underscore, Badge::Trigger.const_get(k)]
+ }.flatten
+ )]
+ end
+
module Trigger
+ None = 0
PostAction = 1
PostRevision = 2
TrustLevelChange = 4
@@ -169,6 +178,11 @@ SQL
scope :enabled, ->{ where(enabled: true) }
+ # fields that can not be edited on system badges
+ def self.protected_system_fields
+ [:badge_type_id, :multiple_grant, :target_posts, :show_posts, :query, :trigger, :auto_revoke, :listable]
+ end
+
def self.trust_level_badge_ids
(1..4).to_a
@@ -191,6 +205,10 @@ SQL
!self.multiple_grant?
end
+ def system?
+ id < 100
+ end
+
def default_name=(val)
self.name ||= val
end
@@ -224,6 +242,7 @@ end
# auto_revoke :boolean default(TRUE), not null
# badge_grouping_id :integer default(5), not null
# trigger :integer
+# show_posts :boolean default(FALSE), not null
#
# Indexes
#
diff --git a/app/serializers/admin_badge_serializer.rb b/app/serializers/admin_badge_serializer.rb
new file mode 100644
index 00000000000..83cccb0c234
--- /dev/null
+++ b/app/serializers/admin_badge_serializer.rb
@@ -0,0 +1,3 @@
+class AdminBadgeSerializer < BadgeSerializer
+ attributes :query, :trigger, :target_posts, :auto_revoke, :show_posts
+end
diff --git a/app/serializers/admin_badges_serializer.rb b/app/serializers/admin_badges_serializer.rb
new file mode 100644
index 00000000000..eedf6fd464d
--- /dev/null
+++ b/app/serializers/admin_badges_serializer.rb
@@ -0,0 +1,14 @@
+class AdminBadgesSerializer < ApplicationSerializer
+ attributes :protected_system_fields, :triggers
+ has_many :badges, serializer: AdminBadgeSerializer
+ has_many :badge_groupings
+ has_many :badge_types
+
+ def protected_system_fields
+ object.protected_system_fields
+ end
+
+ def triggers
+ object.triggers
+ end
+end
diff --git a/app/serializers/badge_serializer.rb b/app/serializers/badge_serializer.rb
index 60a0262189d..1059f9de244 100644
--- a/app/serializers/badge_serializer.rb
+++ b/app/serializers/badge_serializer.rb
@@ -1,5 +1,10 @@
class BadgeSerializer < ApplicationSerializer
attributes :id, :name, :description, :grant_count, :allow_title,
- :multiple_grant, :icon, :listable, :enabled, :badge_grouping_id
+ :multiple_grant, :icon, :listable, :enabled, :badge_grouping_id,
+ :system
has_one :badge_type
+
+ def system
+ object.system?
+ end
end
diff --git a/app/services/badge_granter.rb b/app/services/badge_granter.rb
index 3c5f9c663e8..4a7026ea1c0 100644
--- a/app/services/badge_granter.rb
+++ b/app/services/badge_granter.rb
@@ -122,6 +122,32 @@ class BadgeGranter
"badge_queue".freeze
end
+ def self.preview(sql, opts = {})
+ count_sql = "SELECT COUNT(*) count FROM (#{sql}) q"
+ grant_count = SqlBuilder.map_exec(OpenStruct, count_sql).first.count
+
+ grants_sql =
+ if opts[:target_posts]
+ "SELECT u.id, u.username, q.post_id, t.title, q.granted_at
+ FROM(#{sql}) q
+ JOIN users u on u.id = q.user_id
+ LEFT JOIN badge_posts p on p.id = q.post_id
+ LEFT JOIN topics t on t.id = q.topic_id
+ LIMIT 10"
+ else
+ "SELECT u.id, u.username, q.granted_at
+ FROM(#{sql}) q
+ JOIN users u on u.id = q.user_id
+ LIMIT 10"
+ end
+
+ sample = SqlBuilder.map_exec(OpenStruct, grants_sql).map(&:to_h)
+
+ {grant_count: grant_count, sample: sample}
+ rescue => e
+ {error: e.to_s}
+ end
+
def self.backfill(badge, opts=nil)
return unless badge.query.present? && badge.enabled
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 0753b1387e8..7749d3d35e7 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -1907,6 +1907,18 @@ en:
listable: Show badge on the public badges page
enabled: Enable badge
icon: Icon
+ query: Badge Query (SQL)
+ target_posts: Query targets posts
+ auto_revoke: Run revocation query daily
+ show_posts: Show post granting badge on badge page
+ preview: Preview badge
+ trigger: Trigger
+ trigger_type:
+ none: "Update daily"
+ post_action: "When a user acts on post"
+ post_revision: "When a user edits or creates a post"
+ trust_level_change: "When a user changes trust level"
+ user_change: "When a user is edited or created"
lightbox:
download: "download"
diff --git a/config/routes.rb b/config/routes.rb
index e96276eede6..c38ca989259 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -147,6 +147,7 @@ Discourse::Application.routes.draw do
collection do
get "types" => "badges#badge_types"
get "groupings" => "badges#badge_groupings"
+ post "preview" => "badges#preview"
end
end
diff --git a/test/javascripts/models/badge_test.js b/test/javascripts/models/badge_test.js
index f04279d974b..cd25d79c39f 100644
--- a/test/javascripts/models/badge_test.js
+++ b/test/javascripts/models/badge_test.js
@@ -63,7 +63,8 @@ test('updateFromJson', function() {
test('save', function() {
this.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve({}));
var badge = Discourse.Badge.create({name: "New Badge", description: "This is a new badge.", badge_type_id: 1});
- badge.save();
+ // TODO: clean API
+ badge.save(["name", "description", "badge_type_id"]);
ok(Discourse.ajax.calledOnce, "saved badge");
});